diff --git a/.idea/.idea.osu.Android/.idea/.name b/.idea/.idea.osu.Android/.idea/.name
new file mode 100644
index 0000000000..86363b495c
--- /dev/null
+++ b/.idea/.idea.osu.Android/.idea/.name
@@ -0,0 +1 @@
+osu.Android
\ No newline at end of file
diff --git a/.idea/.idea.osu.Android/.idea/indexLayout.xml b/.idea/.idea.osu.Android/.idea/indexLayout.xml
new file mode 100644
index 0000000000..7b08163ceb
--- /dev/null
+++ b/.idea/.idea.osu.Android/.idea/indexLayout.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="UserContentModel">
+    <attachedFolders />
+    <explicitIncludes />
+    <explicitExcludes />
+  </component>
+</project>
\ No newline at end of file
diff --git a/.idea/.idea.osu.Android/.idea/misc.xml b/.idea/.idea.osu.Android/.idea/misc.xml
new file mode 100644
index 0000000000..1d8c84d0af
--- /dev/null
+++ b/.idea/.idea.osu.Android/.idea/misc.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="com.jetbrains.rider.android.RiderAndroidMiscFileCreationComponent">
+    <option name="ENSURE_MISC_FILE_EXISTS" value="true" />
+  </component>
+</project>
\ No newline at end of file
diff --git a/.idea/.idea.osu.Android/.idea/projectSettingsUpdater.xml b/.idea/.idea.osu.Android/.idea/projectSettingsUpdater.xml
new file mode 100644
index 0000000000..4bb9f4d2a0
--- /dev/null
+++ b/.idea/.idea.osu.Android/.idea/projectSettingsUpdater.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="RiderProjectSettingsUpdater">
+    <option name="vcsConfiguration" value="2" />
+  </component>
+</project>
\ No newline at end of file
diff --git a/.idea/.idea.osu.Android/.idea/vcs.xml b/.idea/.idea.osu.Android/.idea/vcs.xml
new file mode 100644
index 0000000000..94a25f7f4c
--- /dev/null
+++ b/.idea/.idea.osu.Android/.idea/vcs.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="VcsDirectoryMappings">
+    <mapping directory="$PROJECT_DIR$" vcs="Git" />
+  </component>
+</project>
\ No newline at end of file
diff --git a/.idea/.idea.osu/.idea/misc.xml b/.idea/.idea.osu/.idea/misc.xml
new file mode 100644
index 0000000000..1d8c84d0af
--- /dev/null
+++ b/.idea/.idea.osu/.idea/misc.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="com.jetbrains.rider.android.RiderAndroidMiscFileCreationComponent">
+    <option name="ENSURE_MISC_FILE_EXISTS" value="true" />
+  </component>
+</project>
\ No newline at end of file
diff --git a/.idea/.idea.osu/.idea/modules.xml b/.idea/.idea.osu/.idea/modules.xml
deleted file mode 100644
index 0360fdbc5e..0000000000
--- a/.idea/.idea.osu/.idea/modules.xml
+++ /dev/null
@@ -1,8 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<project version="4">
-  <component name="ProjectModuleManager">
-    <modules>
-      <module fileurl="file://$PROJECT_DIR$/.idea/.idea.osu/riderModule.iml" filepath="$PROJECT_DIR$/.idea/.idea.osu/riderModule.iml" />
-    </modules>
-  </component>
-</project>
\ No newline at end of file
diff --git a/.idea/.idea.osu/.idea/projectSettingsUpdater.xml b/.idea/.idea.osu/.idea/projectSettingsUpdater.xml
index 7515e76054..4bb9f4d2a0 100644
--- a/.idea/.idea.osu/.idea/projectSettingsUpdater.xml
+++ b/.idea/.idea.osu/.idea/projectSettingsUpdater.xml
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <project version="4">
   <component name="RiderProjectSettingsUpdater">
-    <option name="vcsConfiguration" value="1" />
+    <option name="vcsConfiguration" value="2" />
   </component>
 </project>
\ No newline at end of file
diff --git a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/TestSceneOsuGame.cs b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/TestSceneOsuGame.cs
index 536fdfc6df..5973db908c 100644
--- a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/TestSceneOsuGame.cs
+++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/TestSceneOsuGame.cs
@@ -4,7 +4,6 @@
 using osu.Framework.Allocation;
 using osu.Framework.Graphics;
 using osu.Framework.Graphics.Shapes;
-using osu.Framework.Platform;
 using osu.Game.Tests.Visual;
 using osuTK.Graphics;
 
@@ -13,7 +12,7 @@ namespace osu.Game.Rulesets.EmptyFreeform.Tests
     public class TestSceneOsuGame : OsuTestScene
     {
         [BackgroundDependencyLoader]
-        private void load(GameHost host, OsuGameBase gameBase)
+        private void load()
         {
             Children = new Drawable[]
             {
diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/TestSceneOsuGame.cs b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/TestSceneOsuGame.cs
index 3cdf44e6f1..b75a5ec187 100644
--- a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/TestSceneOsuGame.cs
+++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/TestSceneOsuGame.cs
@@ -4,7 +4,6 @@
 using osu.Framework.Allocation;
 using osu.Framework.Graphics;
 using osu.Framework.Graphics.Shapes;
-using osu.Framework.Platform;
 using osu.Game.Tests.Visual;
 using osuTK.Graphics;
 
@@ -13,7 +12,7 @@ namespace osu.Game.Rulesets.Pippidon.Tests
     public class TestSceneOsuGame : OsuTestScene
     {
         [BackgroundDependencyLoader]
-        private void load(GameHost host, OsuGameBase gameBase)
+        private void load()
         {
             Children = new Drawable[]
             {
diff --git a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/TestSceneOsuGame.cs b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/TestSceneOsuGame.cs
index 4d3f5086d9..ffe921b54c 100644
--- a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/TestSceneOsuGame.cs
+++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/TestSceneOsuGame.cs
@@ -4,7 +4,6 @@
 using osu.Framework.Allocation;
 using osu.Framework.Graphics;
 using osu.Framework.Graphics.Shapes;
-using osu.Framework.Platform;
 using osu.Game.Tests.Visual;
 using osuTK.Graphics;
 
@@ -13,7 +12,7 @@ namespace osu.Game.Rulesets.EmptyScrolling.Tests
     public class TestSceneOsuGame : OsuTestScene
     {
         [BackgroundDependencyLoader]
-        private void load(GameHost host, OsuGameBase gameBase)
+        private void load()
         {
             Children = new Drawable[]
             {
diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/TestSceneOsuGame.cs b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/TestSceneOsuGame.cs
index 3cdf44e6f1..b75a5ec187 100644
--- a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/TestSceneOsuGame.cs
+++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/TestSceneOsuGame.cs
@@ -4,7 +4,6 @@
 using osu.Framework.Allocation;
 using osu.Framework.Graphics;
 using osu.Framework.Graphics.Shapes;
-using osu.Framework.Platform;
 using osu.Game.Tests.Visual;
 using osuTK.Graphics;
 
@@ -13,7 +12,7 @@ namespace osu.Game.Rulesets.Pippidon.Tests
     public class TestSceneOsuGame : OsuTestScene
     {
         [BackgroundDependencyLoader]
-        private void load(GameHost host, OsuGameBase gameBase)
+        private void load()
         {
             Children = new Drawable[]
             {
diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/UI/PippidonPlayfield.cs b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/UI/PippidonPlayfield.cs
index 0e50030162..ab8c6bb2e9 100644
--- a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/UI/PippidonPlayfield.cs
+++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/UI/PippidonPlayfield.cs
@@ -7,7 +7,6 @@ using osu.Framework.Bindables;
 using osu.Framework.Graphics;
 using osu.Framework.Graphics.Containers;
 using osu.Framework.Graphics.Shapes;
-using osu.Framework.Graphics.Textures;
 using osu.Game.Beatmaps.ControlPoints;
 using osu.Game.Graphics;
 using osu.Game.Graphics.Containers;
@@ -28,7 +27,7 @@ namespace osu.Game.Rulesets.Pippidon.UI
         private PippidonCharacter pippidon;
 
         [BackgroundDependencyLoader]
-        private void load(TextureStore textures)
+        private void load()
         {
             AddRangeInternal(new Drawable[]
             {
diff --git a/osu.Android.props b/osu.Android.props
index 4198cf2bf4..b296c114e9 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -51,11 +51,11 @@
     <Reference Include="Java.Interop" />
   </ItemGroup>
   <ItemGroup>
-    <PackageReference Include="ppy.osu.Game.Resources" Version="2022.114.0" />
-    <PackageReference Include="ppy.osu.Framework.Android" Version="2022.111.0" />
+    <PackageReference Include="ppy.osu.Game.Resources" Version="2022.115.0" />
+    <PackageReference Include="ppy.osu.Framework.Android" Version="2022.118.0" />
   </ItemGroup>
   <ItemGroup Label="Transitive Dependencies">
     <!-- Realm needs to be directly referenced in all Xamarin projects, as it will not pull in its transitive dependencies otherwise. -->
-    <PackageReference Include="Realm" Version="10.7.1" />
+    <PackageReference Include="Realm" Version="10.8.0" />
   </ItemGroup>
 </Project>
diff --git a/osu.Desktop/OsuGameDesktop.cs b/osu.Desktop/OsuGameDesktop.cs
index b234207848..cd3fb7eb61 100644
--- a/osu.Desktop/OsuGameDesktop.cs
+++ b/osu.Desktop/OsuGameDesktop.cs
@@ -10,14 +10,11 @@ using System.Runtime.Versioning;
 using System.Threading.Tasks;
 using Microsoft.Win32;
 using osu.Desktop.Security;
-using osu.Desktop.Overlays;
 using osu.Framework.Platform;
 using osu.Game;
 using osu.Desktop.Updater;
 using osu.Framework;
 using osu.Framework.Logging;
-using osu.Framework.Screens;
-using osu.Game.Screens.Menu;
 using osu.Game.Updater;
 using osu.Desktop.Windows;
 using osu.Framework.Threading;
@@ -27,13 +24,9 @@ namespace osu.Desktop
 {
     internal class OsuGameDesktop : OsuGame
     {
-        private readonly bool noVersionOverlay;
-        private VersionManager versionManager;
-
         public OsuGameDesktop(string[] args = null)
             : base(args)
         {
-            noVersionOverlay = args?.Any(a => a == "--no-version-overlay") ?? false;
         }
 
         public override StableStorage GetStorageForStableInstall()
@@ -114,9 +107,6 @@ namespace osu.Desktop
         {
             base.LoadComplete();
 
-            if (!noVersionOverlay)
-                LoadComponentAsync(versionManager = new VersionManager { Depth = int.MinValue }, ScreenContainer.Add);
-
             LoadComponentAsync(new DiscordRichPresence(), Add);
 
             if (RuntimeInfo.OS == RuntimeInfo.Platform.Windows)
@@ -125,23 +115,6 @@ namespace osu.Desktop
             LoadComponentAsync(new ElevatedPrivilegesChecker(), Add);
         }
 
-        protected override void ScreenChanged(IScreen lastScreen, IScreen newScreen)
-        {
-            base.ScreenChanged(lastScreen, newScreen);
-
-            switch (newScreen)
-            {
-                case IntroScreen _:
-                case MainMenu _:
-                    versionManager?.Show();
-                    break;
-
-                default:
-                    versionManager?.Hide();
-                    break;
-            }
-        }
-
         public override void SetHost(GameHost host)
         {
             base.SetHost(host);
diff --git a/osu.Desktop/Security/ElevatedPrivilegesChecker.cs b/osu.Desktop/Security/ElevatedPrivilegesChecker.cs
index 62ea3e0399..8f3ad853dc 100644
--- a/osu.Desktop/Security/ElevatedPrivilegesChecker.cs
+++ b/osu.Desktop/Security/ElevatedPrivilegesChecker.cs
@@ -73,7 +73,7 @@ namespace osu.Desktop.Security
             }
 
             [BackgroundDependencyLoader]
-            private void load(OsuColour colours, NotificationOverlay notificationOverlay)
+            private void load(OsuColour colours)
             {
                 Icon = FontAwesome.Solid.ShieldAlt;
                 IconBackground.Colour = colours.YellowDark;
diff --git a/osu.Game.Rulesets.Catch.Tests/Editor/CatchSelectionBlueprintTestScene.cs b/osu.Game.Rulesets.Catch.Tests/Editor/CatchSelectionBlueprintTestScene.cs
index d4c2c0f0af..e345e03c96 100644
--- a/osu.Game.Rulesets.Catch.Tests/Editor/CatchSelectionBlueprintTestScene.cs
+++ b/osu.Game.Rulesets.Catch.Tests/Editor/CatchSelectionBlueprintTestScene.cs
@@ -29,7 +29,13 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
 
         protected CatchSelectionBlueprintTestScene()
         {
-            EditorBeatmap = new EditorBeatmap(new CatchBeatmap()) { Difficulty = { CircleSize = 0 } };
+            EditorBeatmap = new EditorBeatmap(new CatchBeatmap
+            {
+                BeatmapInfo =
+                {
+                    Ruleset = new CatchRuleset().RulesetInfo,
+                }
+            }) { Difficulty = { CircleSize = 0 } };
             EditorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint
             {
                 BeatLength = 100
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneAutoJuiceStream.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneAutoJuiceStream.cs
index f552c3c27b..1014158fc1 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneAutoJuiceStream.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneAutoJuiceStream.cs
@@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Catch.Tests
             {
                 BeatmapInfo = new BeatmapInfo
                 {
-                    BaseDifficulty = new BeatmapDifficulty { CircleSize = 6, SliderMultiplier = 3 },
+                    Difficulty = new BeatmapDifficulty { CircleSize = 6, SliderMultiplier = 3 },
                     Ruleset = ruleset
                 }
             };
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneBananaShower.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneBananaShower.cs
index e89a95ae37..96ac5c4bf2 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneBananaShower.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneBananaShower.cs
@@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Catch.Tests
             {
                 BeatmapInfo = new BeatmapInfo
                 {
-                    BaseDifficulty = new BeatmapDifficulty { CircleSize = 6 },
+                    Difficulty = new BeatmapDifficulty { CircleSize = 6 },
                     Ruleset = ruleset
                 }
             };
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchStacker.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchStacker.cs
index 1ff31697b8..0a4ef49e19 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchStacker.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchStacker.cs
@@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Catch.Tests
             {
                 BeatmapInfo = new BeatmapInfo
                 {
-                    BaseDifficulty = new BeatmapDifficulty { CircleSize = 6 },
+                    Difficulty = new BeatmapDifficulty { CircleSize = 6 },
                     Ruleset = ruleset
                 }
             };
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjects.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjects.cs
index 23f6222eb6..4b8fede369 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjects.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjects.cs
@@ -35,12 +35,12 @@ namespace osu.Game.Rulesets.Catch.Tests
                 HitObjects = new List<HitObject> { new Fruit() },
                 BeatmapInfo = new BeatmapInfo
                 {
-                    BaseDifficulty = new BeatmapDifficulty(),
+                    Difficulty = new BeatmapDifficulty(),
                     Metadata = new BeatmapMetadata
                     {
                         Artist = @"Unknown",
                         Title = @"You're breathtaking",
-                        AuthorString = @"Everyone",
+                        Author = { Username = @"Everyone" },
                     },
                     Ruleset = new CatchRuleset().RulesetInfo
                 },
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs
index 163fee49fb..a5b44dc605 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs
@@ -63,7 +63,7 @@ namespace osu.Game.Rulesets.Catch.Tests
                 BeatmapInfo =
                 {
                     Ruleset = ruleset,
-                    BaseDifficulty = new BeatmapDifficulty { CircleSize = 3.6f }
+                    Difficulty = new BeatmapDifficulty { CircleSize = 3.6f }
                 }
             };
 
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneJuiceStream.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneJuiceStream.cs
index 269e783899..4601234669 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneJuiceStream.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneJuiceStream.cs
@@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Catch.Tests
         {
             BeatmapInfo = new BeatmapInfo
             {
-                BaseDifficulty = new BeatmapDifficulty { CircleSize = 5, SliderMultiplier = 2 },
+                Difficulty = new BeatmapDifficulty { CircleSize = 5, SliderMultiplier = 2 },
                 Ruleset = ruleset
             },
             HitObjects = new List<HitObject>
diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaBeatSnapGrid.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaBeatSnapGrid.cs
index 5ccb191a9b..50be13c4e0 100644
--- a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaBeatSnapGrid.cs
+++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaBeatSnapGrid.cs
@@ -29,7 +29,13 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
         private ScrollingTestContainer.TestScrollingInfo scrollingInfo = new ScrollingTestContainer.TestScrollingInfo();
 
         [Cached(typeof(EditorBeatmap))]
-        private EditorBeatmap editorBeatmap = new EditorBeatmap(new ManiaBeatmap(new StageDefinition()));
+        private EditorBeatmap editorBeatmap = new EditorBeatmap(new ManiaBeatmap(new StageDefinition())
+        {
+            BeatmapInfo =
+            {
+                Ruleset = new ManiaRuleset().RulesetInfo
+            }
+        });
 
         private readonly ManiaBeatSnapGrid beatSnapGrid;
 
diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaComposeScreen.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaComposeScreen.cs
index a30e09cd29..5dd7c23ab6 100644
--- a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaComposeScreen.cs
+++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaComposeScreen.cs
@@ -31,10 +31,10 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
         {
             AddStep("setup compose screen", () =>
             {
-                var editorBeatmap = new EditorBeatmap(new ManiaBeatmap(new StageDefinition { Columns = 4 }))
+                var editorBeatmap = new EditorBeatmap(new ManiaBeatmap(new StageDefinition { Columns = 4 })
                 {
                     BeatmapInfo = { Ruleset = new ManiaRuleset().RulesetInfo },
-                };
+                });
 
                 Beatmap.Value = CreateWorkingBeatmap(editorBeatmap.PlayableBeatmap);
 
diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaHitObjectComposer.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaHitObjectComposer.cs
index 01d80881fa..9788dfe844 100644
--- a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaHitObjectComposer.cs
+++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaHitObjectComposer.cs
@@ -203,10 +203,10 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
             {
                 InternalChildren = new Drawable[]
                 {
-                    EditorBeatmap = new EditorBeatmap(new ManiaBeatmap(new StageDefinition { Columns = 4 }))
+                    EditorBeatmap = new EditorBeatmap(new ManiaBeatmap(new StageDefinition { Columns = 4 })
                     {
                         BeatmapInfo = { Ruleset = new ManiaRuleset().RulesetInfo }
-                    },
+                    }),
                     Composer = new ManiaHitObjectComposer(new ManiaRuleset())
                 };
 
diff --git a/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/skin.ini b/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/skin.ini
index 36765d61bf..9c987efc60 100644
--- a/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/skin.ini
+++ b/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/skin.ini
@@ -4,11 +4,14 @@ Version: 2.5
 [Mania]
 Keys: 4
 ColumnLineWidth: 3,1,3,1,1
-Hit0: mania/hit0
-Hit50: mania/hit50
-Hit100: mania/hit100
-Hit200: mania/hit200
-Hit300: mania/hit300
-Hit300g: mania/hit300g
+// some skins found in the wild had configuration keys where the @2x suffix was included in the values.
+// the expected compatibility behaviour is that the presence of the @2x suffix shouldn't change anything
+// if @2x assets are present.
+Hit0: mania/hit0@2x
+Hit50: mania/hit50@2x
+Hit100: mania/hit100@2x
+Hit200: mania/hit200@2x
+Hit300: mania/hit300@2x
+Hit300g: mania/hit300g@2x
 StageLeft: mania/stage-left
 StageRight: mania/stage-right
\ No newline at end of file
diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneDrawableJudgement.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneDrawableJudgement.cs
index 75a5495078..d033676ec7 100644
--- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneDrawableJudgement.cs
+++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneDrawableJudgement.cs
@@ -5,8 +5,10 @@ using System;
 using System.Linq;
 using osu.Framework.Extensions;
 using osu.Framework.Graphics;
+using osu.Framework.Testing;
 using osu.Game.Rulesets.Judgements;
 using osu.Game.Rulesets.Mania.Scoring;
+using osu.Game.Rulesets.Mania.Skinning.Legacy;
 using osu.Game.Rulesets.Mania.UI;
 using osu.Game.Rulesets.Objects;
 using osu.Game.Rulesets.Scoring;
@@ -23,15 +25,24 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
             {
                 if (hitWindows.IsHitResultAllowed(result))
                 {
-                    AddStep("Show " + result.GetDescription(), () => SetContents(_ =>
-                        new DrawableManiaJudgement(new JudgementResult(new HitObject { StartTime = Time.Current }, new Judgement())
-                        {
-                            Type = result
-                        }, null)
-                        {
-                            Anchor = Anchor.Centre,
-                            Origin = Anchor.Centre,
-                        }));
+                    AddStep("Show " + result.GetDescription(), () =>
+                    {
+                        SetContents(_ =>
+                            new DrawableManiaJudgement(new JudgementResult(new HitObject { StartTime = Time.Current }, new Judgement())
+                            {
+                                Type = result
+                            }, null)
+                            {
+                                Anchor = Anchor.Centre,
+                                Origin = Anchor.Centre,
+                            });
+
+                        // for test purposes, undo the Y adjustment related to the `ScorePosition` legacy positioning config value
+                        // (see `LegacyManiaJudgementPiece.load()`).
+                        // this prevents the judgements showing somewhere below or above the bounding box of the judgement.
+                        foreach (var legacyPiece in this.ChildrenOfType<LegacyManiaJudgementPiece>())
+                            legacyPiece.Y = 0;
+                    });
                 }
             }
         }
diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs
index 4387bc6b3b..f973cb5ed3 100644
--- a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs
+++ b/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs
@@ -264,7 +264,7 @@ namespace osu.Game.Rulesets.Mania.Tests
                 },
                 BeatmapInfo =
                 {
-                    BaseDifficulty = new BeatmapDifficulty
+                    Difficulty = new BeatmapDifficulty
                     {
                         SliderTickRate = 4,
                         OverallDifficulty = 10,
@@ -306,7 +306,7 @@ namespace osu.Game.Rulesets.Mania.Tests
                 },
                 BeatmapInfo =
                 {
-                    BaseDifficulty = new BeatmapDifficulty { SliderTickRate = tick_rate },
+                    Difficulty = new BeatmapDifficulty { SliderTickRate = tick_rate },
                     Ruleset = new ManiaRuleset().RulesetInfo
                 },
             };
@@ -383,7 +383,7 @@ namespace osu.Game.Rulesets.Mania.Tests
                     },
                     BeatmapInfo =
                     {
-                        BaseDifficulty = new BeatmapDifficulty { SliderTickRate = 4 },
+                        Difficulty = new BeatmapDifficulty { SliderTickRate = 4 },
                         Ruleset = new ManiaRuleset().RulesetInfo
                     },
                 };
diff --git a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs
index 9d0aaec2ba..47e0e6d7b1 100644
--- a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs
+++ b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs
@@ -73,7 +73,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
 
         public static int GetColumnCountForNonConvert(BeatmapInfo beatmapInfo)
         {
-            double roundedCircleSize = Math.Round(beatmapInfo.BaseDifficulty.CircleSize);
+            double roundedCircleSize = Math.Round(beatmapInfo.Difficulty.CircleSize);
             return (int)Math.Max(1, roundedCircleSize);
         }
 
diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteSelectionBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteSelectionBlueprint.cs
index 5259fcbd5f..35889aea0c 100644
--- a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteSelectionBlueprint.cs
+++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteSelectionBlueprint.cs
@@ -9,7 +9,6 @@ using osu.Framework.Graphics.Shapes;
 using osu.Game.Graphics;
 using osu.Game.Rulesets.Mania.Edit.Blueprints.Components;
 using osu.Game.Rulesets.Mania.Objects;
-using osu.Game.Rulesets.UI.Scrolling;
 using osuTK;
 
 namespace osu.Game.Rulesets.Mania.Edit.Blueprints
@@ -28,7 +27,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
         }
 
         [BackgroundDependencyLoader]
-        private void load(IScrollingInfo scrollingInfo)
+        private void load()
         {
             InternalChildren = new Drawable[]
             {
diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/Checks/CheckTooShortSpinnersTest.cs b/osu.Game.Rulesets.Osu.Tests/Editor/Checks/CheckTooShortSpinnersTest.cs
index 787807a8ea..1f3d4297f1 100644
--- a/osu.Game.Rulesets.Osu.Tests/Editor/Checks/CheckTooShortSpinnersTest.cs
+++ b/osu.Game.Rulesets.Osu.Tests/Editor/Checks/CheckTooShortSpinnersTest.cs
@@ -107,7 +107,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor.Checks
             var beatmap = new Beatmap<HitObject>
             {
                 HitObjects = hitObjects,
-                BeatmapInfo = new BeatmapInfo { BaseDifficulty = new BeatmapDifficulty(beatmapDifficulty) }
+                BeatmapInfo = new BeatmapInfo { Difficulty = new BeatmapDifficulty(beatmapDifficulty) }
             };
 
             return new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap));
diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuDistanceSnapGrid.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuDistanceSnapGrid.cs
index ef43c3a696..c770e2d96f 100644
--- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuDistanceSnapGrid.cs
+++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuDistanceSnapGrid.cs
@@ -40,7 +40,13 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
 
         public TestSceneOsuDistanceSnapGrid()
         {
-            editorBeatmap = new EditorBeatmap(new OsuBeatmap());
+            editorBeatmap = new EditorBeatmap(new OsuBeatmap
+            {
+                BeatmapInfo =
+                {
+                    Ruleset = new OsuRuleset().RulesetInfo
+                }
+            });
         }
 
         [SetUp]
diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDifficultyAdjust.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDifficultyAdjust.cs
index db8546c71b..9d06ff5801 100644
--- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDifficultyAdjust.cs
+++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDifficultyAdjust.cs
@@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
             {
                 BeatmapInfo = new BeatmapInfo
                 {
-                    BaseDifficulty = new BeatmapDifficulty
+                    Difficulty = new BeatmapDifficulty
                     {
                         CircleSize = 8
                     }
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleLongCombo.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleLongCombo.cs
index 8cf29ddfbf..4e17c4c363 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleLongCombo.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleLongCombo.cs
@@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Osu.Tests
             {
                 BeatmapInfo = new BeatmapInfo
                 {
-                    BaseDifficulty = new BeatmapDifficulty { CircleSize = 6 },
+                    Difficulty = new BeatmapDifficulty { CircleSize = 6 },
                     Ruleset = ruleset
                 }
             };
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneNoSpinnerStacking.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneNoSpinnerStacking.cs
index ef05bcd320..5e92bac986 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneNoSpinnerStacking.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneNoSpinnerStacking.cs
@@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Osu.Tests
             {
                 BeatmapInfo = new BeatmapInfo
                 {
-                    BaseDifficulty = new BeatmapDifficulty { OverallDifficulty = 10 },
+                    Difficulty = new BeatmapDifficulty { OverallDifficulty = 10 },
                     Ruleset = ruleset
                 }
             };
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs
index f3392724ec..2368cc7365 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs
@@ -358,7 +358,7 @@ namespace osu.Game.Rulesets.Osu.Tests
                     },
                     BeatmapInfo =
                     {
-                        BaseDifficulty = new BeatmapDifficulty { SliderTickRate = 3 },
+                        Difficulty = new BeatmapDifficulty { SliderTickRate = 3 },
                         Ruleset = new OsuRuleset().RulesetInfo
                     },
                 });
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneStartTimeOrderedHitPolicy.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneStartTimeOrderedHitPolicy.cs
index 2d43e1b95e..53fa3624b8 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneStartTimeOrderedHitPolicy.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneStartTimeOrderedHitPolicy.cs
@@ -364,7 +364,7 @@ namespace osu.Game.Rulesets.Osu.Tests
                     HitObjects = hitObjects,
                     BeatmapInfo =
                     {
-                        BaseDifficulty = new BeatmapDifficulty { SliderTickRate = 3 },
+                        Difficulty = new BeatmapDifficulty { SliderTickRate = 3 },
                         Ruleset = new OsuRuleset().RulesetInfo
                     },
                 });
diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs
index fdf646ef85..604ab73454 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs
@@ -25,7 +25,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
         private int countMeh;
         private int countMiss;
 
-        private int effectiveMissCount;
+        private double effectiveMissCount;
 
         public OsuPerformanceCalculator(Ruleset ruleset, DifficultyAttributes attributes, ScoreInfo score)
             : base(ruleset, attributes, score)
@@ -97,7 +97,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
 
             // Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses.
             if (effectiveMissCount > 0)
-                aimValue *= 0.97 * Math.Pow(1 - Math.Pow((double)effectiveMissCount / totalHits, 0.775), effectiveMissCount);
+                aimValue *= 0.97 * Math.Pow(1 - Math.Pow(effectiveMissCount / totalHits, 0.775), effectiveMissCount);
 
             aimValue *= getComboScalingFactor();
 
@@ -144,7 +144,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
 
             // Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses.
             if (effectiveMissCount > 0)
-                speedValue *= 0.97 * Math.Pow(1 - Math.Pow((double)effectiveMissCount / totalHits, 0.775), Math.Pow(effectiveMissCount, .875));
+                speedValue *= 0.97 * Math.Pow(1 - Math.Pow(effectiveMissCount / totalHits, 0.775), Math.Pow(effectiveMissCount, .875));
 
             speedValue *= getComboScalingFactor();
 
@@ -228,7 +228,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
 
             // Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses.
             if (effectiveMissCount > 0)
-                flashlightValue *= 0.97 * Math.Pow(1 - Math.Pow((double)effectiveMissCount / totalHits, 0.775), Math.Pow(effectiveMissCount, .875));
+                flashlightValue *= 0.97 * Math.Pow(1 - Math.Pow(effectiveMissCount / totalHits, 0.775), Math.Pow(effectiveMissCount, .875));
 
             flashlightValue *= getComboScalingFactor();
 
@@ -244,7 +244,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
             return flashlightValue;
         }
 
-        private int calculateEffectiveMissCount()
+        private double calculateEffectiveMissCount()
         {
             // Guess the number of misses + slider breaks from combo
             double comboBasedMissCount = 0.0;
@@ -256,10 +256,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty
                     comboBasedMissCount = fullComboThreshold / Math.Max(1.0, scoreMaxCombo);
             }
 
-            // Clamp misscount since it's derived from combo and can be higher than total hits and that breaks some calculations
+            // Clamp miss count since it's derived from combo and can be higher than total hits and that breaks some calculations
             comboBasedMissCount = Math.Min(comboBasedMissCount, totalHits);
 
-            return Math.Max(countMiss, (int)Math.Floor(comboBasedMissCount));
+            return Math.Max(countMiss, comboBasedMissCount);
         }
 
         private double getComboScalingFactor() => Attributes.MaxCombo <= 0 ? 1.0 : Math.Min(Math.Pow(scoreMaxCombo, 0.8) / Math.Pow(Attributes.MaxCombo, 0.8), 1.0);
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs
index 07b6a1bdc2..b868c9a7ee 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs
@@ -9,7 +9,6 @@ using osu.Framework.Graphics;
 using osu.Framework.Input;
 using osu.Framework.Input.Events;
 using osu.Game.Beatmaps.ControlPoints;
-using osu.Game.Graphics;
 using osu.Game.Rulesets.Edit;
 using osu.Game.Rulesets.Objects;
 using osu.Game.Rulesets.Objects.Types;
@@ -50,7 +49,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
         }
 
         [BackgroundDependencyLoader]
-        private void load(OsuColour colours)
+        private void load()
         {
             InternalChildren = new Drawable[]
             {
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs
index ec87d3bfdf..c6db02ee02 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs
@@ -10,7 +10,6 @@ using osu.Framework.Bindables;
 using osu.Framework.Graphics;
 using osu.Framework.Graphics.Containers;
 using osu.Game.Audio;
-using osu.Game.Graphics;
 using osu.Game.Rulesets.Judgements;
 using osu.Game.Rulesets.Objects;
 using osu.Game.Rulesets.Objects.Drawables;
@@ -69,7 +68,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
         }
 
         [BackgroundDependencyLoader]
-        private void load(OsuColour colours)
+        private void load()
         {
             Origin = Anchor.Centre;
             RelativeSizeAxes = Axes.Both;
diff --git a/osu.Game.Rulesets.Osu/Objects/Spinner.cs b/osu.Game.Rulesets.Osu/Objects/Spinner.cs
index 0ad8e4ea68..1eddfb7fef 100644
--- a/osu.Game.Rulesets.Osu/Objects/Spinner.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Spinner.cs
@@ -65,8 +65,8 @@ namespace osu.Game.Rulesets.Osu.Objects
                 double startTime = StartTime + (float)(i + 1) / totalSpins * Duration;
 
                 AddNested(i < SpinsRequired
-                    ? new SpinnerTick { StartTime = startTime }
-                    : new SpinnerBonusTick { StartTime = startTime });
+                    ? new SpinnerTick { StartTime = startTime, Position = Position }
+                    : new SpinnerBonusTick { StartTime = startTime, Position = Position });
             }
         }
 
diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerBackgroundLayer.cs b/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerBackgroundLayer.cs
index f8a6e1d3c9..a1184a15cd 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerBackgroundLayer.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerBackgroundLayer.cs
@@ -3,15 +3,13 @@
 
 using osu.Framework.Allocation;
 using osu.Framework.Graphics;
-using osu.Game.Graphics;
-using osu.Game.Rulesets.Objects.Drawables;
 
 namespace osu.Game.Rulesets.Osu.Skinning.Default
 {
     public class SpinnerBackgroundLayer : SpinnerFill
     {
         [BackgroundDependencyLoader]
-        private void load(OsuColour colours, DrawableHitObject drawableHitObject)
+        private void load()
         {
             Disc.Alpha = 0;
             Anchor = Anchor.Centre;
diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyCursorParticles.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyCursorParticles.cs
index 611ddd08eb..b511444c44 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyCursorParticles.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyCursorParticles.cs
@@ -40,7 +40,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
         private GameplayState gameplayState { get; set; }
 
         [BackgroundDependencyLoader]
-        private void load(ISkinSource skin, OsuColour colours)
+        private void load(ISkinSource skin)
         {
             var texture = skin.GetTexture("star2");
             var starBreakAdditive = skin.GetConfig<OsuSkinColour, Color4>(OsuSkinColour.StarBreakAdditive)?.Value ?? new Color4(255, 182, 193, 255);
diff --git a/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs b/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs
index db4a6eb50b..6c76da7925 100644
--- a/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs
+++ b/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs
@@ -174,7 +174,7 @@ namespace osu.Game.Rulesets.Osu.Statistics
 
             pointGrid.Content = points;
 
-            if (score.HitEvents == null || score.HitEvents.Count == 0)
+            if (score.HitEvents.Count == 0)
                 return;
 
             // Todo: This should probably not be done like this.
diff --git a/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorContainer.cs b/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorContainer.cs
index d1d9ee9f4d..b60ea5da21 100644
--- a/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorContainer.cs
+++ b/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorContainer.cs
@@ -58,7 +58,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
         private OsuConfigManager config { get; set; }
 
         [BackgroundDependencyLoader(true)]
-        private void load(OsuConfigManager config, OsuRulesetConfigManager rulesetConfig)
+        private void load(OsuRulesetConfigManager rulesetConfig)
         {
             rulesetConfig?.BindWith(OsuRulesetSetting.ShowCursorTrail, showTrail);
         }
diff --git a/osu.Game.Rulesets.Taiko.Tests/DrawableTaikoRulesetTestScene.cs b/osu.Game.Rulesets.Taiko.Tests/DrawableTaikoRulesetTestScene.cs
index 4bdb85ba60..f5e7304c12 100644
--- a/osu.Game.Rulesets.Taiko.Tests/DrawableTaikoRulesetTestScene.cs
+++ b/osu.Game.Rulesets.Taiko.Tests/DrawableTaikoRulesetTestScene.cs
@@ -32,12 +32,12 @@ namespace osu.Game.Rulesets.Taiko.Tests
                 HitObjects = new List<HitObject> { new Hit { Type = HitType.Centre } },
                 BeatmapInfo = new BeatmapInfo
                 {
-                    BaseDifficulty = new BeatmapDifficulty(),
+                    Difficulty = new BeatmapDifficulty(),
                     Metadata = new BeatmapMetadata
                     {
                         Artist = @"Unknown",
                         Title = @"Sample Beatmap",
-                        AuthorString = @"peppy",
+                        Author = { Username = @"peppy" },
                     },
                     Ruleset = new TaikoRuleset().RulesetInfo
                 },
diff --git a/osu.Game.Rulesets.Taiko.Tests/Editor/TestSceneTaikoHitObjectComposer.cs b/osu.Game.Rulesets.Taiko.Tests/Editor/TestSceneTaikoHitObjectComposer.cs
index 626537053a..55eb2fa66b 100644
--- a/osu.Game.Rulesets.Taiko.Tests/Editor/TestSceneTaikoHitObjectComposer.cs
+++ b/osu.Game.Rulesets.Taiko.Tests/Editor/TestSceneTaikoHitObjectComposer.cs
@@ -40,10 +40,10 @@ namespace osu.Game.Rulesets.Taiko.Tests.Editor
             {
                 InternalChildren = new Drawable[]
                 {
-                    EditorBeatmap = new EditorBeatmap(new TaikoBeatmap())
+                    EditorBeatmap = new EditorBeatmap(new TaikoBeatmap
                     {
                         BeatmapInfo = { Ruleset = new TaikoRuleset().RulesetInfo }
-                    },
+                    }),
                     new TaikoHitObjectComposer(new TaikoRuleset())
                 };
 
diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs
index b976735223..920a7cd1a1 100644
--- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs
+++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs
@@ -158,12 +158,12 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
                 HitObjects = new List<HitObject> { new Hit { Type = HitType.Centre } },
                 BeatmapInfo = new BeatmapInfo
                 {
-                    BaseDifficulty = new BeatmapDifficulty(),
+                    Difficulty = new BeatmapDifficulty(),
                     Metadata = new BeatmapMetadata
                     {
                         Artist = "Unknown",
                         Title = "Sample Beatmap",
-                        AuthorString = "Craftplacer",
+                        Author = { Username = "Craftplacer" },
                     },
                     Ruleset = new TaikoRuleset().RulesetInfo
                 },
diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneDrumRollJudgements.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneDrumRollJudgements.cs
new file mode 100644
index 0000000000..060c3c9443
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneDrumRollJudgements.cs
@@ -0,0 +1,36 @@
+// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Linq;
+using NUnit.Framework;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Taiko.Objects;
+
+namespace osu.Game.Rulesets.Taiko.Tests
+{
+    public class TestSceneDrumRollJudgements : TestSceneTaikoPlayer
+    {
+        [Test]
+        public void TestStrongDrumRollFullyJudgedOnKilled()
+        {
+            AddUntilStep("gameplay finished", () => Player.ScoreProcessor.HasCompleted.Value);
+            AddAssert("all judgements are misses", () => Player.Results.All(r => r.Type == r.Judgement.MinResult));
+        }
+
+        protected override bool Autoplay => false;
+
+        protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new Beatmap<TaikoHitObject>
+        {
+            BeatmapInfo = { Ruleset = new TaikoRuleset().RulesetInfo },
+            HitObjects =
+            {
+                new DrumRoll
+                {
+                    StartTime = 1000,
+                    Duration = 1000,
+                    IsStrong = true
+                }
+            }
+        };
+    }
+}
diff --git a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs
index 9b2e9fedc5..613874b7d6 100644
--- a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs
+++ b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs
@@ -191,6 +191,9 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
 
         protected override Beatmap<TaikoHitObject> CreateBeatmap() => new TaikoBeatmap();
 
+        // Important to note that this is subclassing a realm object.
+        // Realm doesn't allow this, but for now this can work since we aren't (in theory?) persisting this to the database.
+        // It is only used during beatmap conversion and processing.
         internal class TaikoMultiplierAppliedDifficulty : BeatmapDifficulty
         {
             public TaikoMultiplierAppliedDifficulty(IBeatmapDifficultyInfo difficulty)
@@ -205,6 +208,8 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
 
             #region Overrides of BeatmapDifficulty
 
+            public override BeatmapDifficulty Clone() => new TaikoMultiplierAppliedDifficulty(this);
+
             public override void CopyTo(BeatmapDifficulty other)
             {
                 base.CopyTo(other);
diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs
index 521189d36c..b84db513f7 100644
--- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs
+++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs
@@ -197,6 +197,14 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
                 ApplyResult(r => r.Type = ParentHitObject.IsHit ? r.Judgement.MaxResult : r.Judgement.MinResult);
             }
 
+            public override void OnKilled()
+            {
+                base.OnKilled();
+
+                if (Time.Current > ParentHitObject.HitObject.GetEndTime() && !Judged)
+                    ApplyResult(r => r.Type = r.Judgement.MinResult);
+            }
+
             public override bool OnPressed(KeyBindingPressEvent<TaikoAction> e) => false;
         }
     }
diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs
index dc2ed200a1..e24923e482 100644
--- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs
+++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs
@@ -5,6 +5,7 @@ using System;
 using JetBrains.Annotations;
 using osu.Framework.Graphics;
 using osu.Framework.Input.Events;
+using osu.Game.Rulesets.Objects;
 using osu.Game.Rulesets.Objects.Drawables;
 using osu.Game.Rulesets.Taiko.Skinning.Default;
 using osu.Game.Skinning;
@@ -52,6 +53,14 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
             ApplyResult(r => r.Type = r.Judgement.MaxResult);
         }
 
+        public override void OnKilled()
+        {
+            base.OnKilled();
+
+            if (Time.Current > HitObject.GetEndTime() && !Judged)
+                ApplyResult(r => r.Type = r.Judgement.MinResult);
+        }
+
         protected override void UpdateHitStateTransforms(ArmedState state)
         {
             switch (state)
@@ -92,6 +101,14 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
                 ApplyResult(r => r.Type = ParentHitObject.IsHit ? r.Judgement.MaxResult : r.Judgement.MinResult);
             }
 
+            public override void OnKilled()
+            {
+                base.OnKilled();
+
+                if (Time.Current > ParentHitObject.HitObject.GetEndTime() && !Judged)
+                    ApplyResult(r => r.Type = r.Judgement.MinResult);
+            }
+
             public override bool OnPressed(KeyBindingPressEvent<TaikoAction> e) => false;
         }
     }
diff --git a/osu.Game.Rulesets.Taiko/Skinning/Default/CentreHitCirclePiece.cs b/osu.Game.Rulesets.Taiko/Skinning/Default/CentreHitCirclePiece.cs
index 455b2fc596..25f895708f 100644
--- a/osu.Game.Rulesets.Taiko/Skinning/Default/CentreHitCirclePiece.cs
+++ b/osu.Game.Rulesets.Taiko/Skinning/Default/CentreHitCirclePiece.cs
@@ -5,7 +5,6 @@ using osu.Framework.Allocation;
 using osu.Framework.Graphics;
 using osu.Framework.Graphics.Containers;
 using osu.Framework.Graphics.Shapes;
-using osu.Game.Graphics;
 using osu.Game.Rulesets.Taiko.Objects;
 using osuTK;
 
@@ -19,7 +18,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Default
         }
 
         [BackgroundDependencyLoader]
-        private void load(OsuColour colours)
+        private void load()
         {
             AccentColour = Hit.COLOUR_CENTRE;
         }
diff --git a/osu.Game.Rulesets.Taiko/Skinning/Default/RimHitCirclePiece.cs b/osu.Game.Rulesets.Taiko/Skinning/Default/RimHitCirclePiece.cs
index bd21d511b1..c6165495d8 100644
--- a/osu.Game.Rulesets.Taiko/Skinning/Default/RimHitCirclePiece.cs
+++ b/osu.Game.Rulesets.Taiko/Skinning/Default/RimHitCirclePiece.cs
@@ -5,7 +5,6 @@ using osu.Framework.Allocation;
 using osu.Framework.Graphics;
 using osu.Framework.Graphics.Containers;
 using osu.Framework.Graphics.Shapes;
-using osu.Game.Graphics;
 using osu.Game.Rulesets.Taiko.Objects;
 using osuTK;
 using osuTK.Graphics;
@@ -20,7 +19,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Default
         }
 
         [BackgroundDependencyLoader]
-        private void load(OsuColour colours)
+        private void load()
         {
             AccentColour = Hit.COLOUR_RIM;
         }
diff --git a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoMascot.cs b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoMascot.cs
index e1063e1071..7ba2618a63 100644
--- a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoMascot.cs
+++ b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoMascot.cs
@@ -7,7 +7,6 @@ using osu.Framework.Audio.Track;
 using osu.Framework.Bindables;
 using osu.Framework.Extensions.IEnumerableExtensions;
 using osu.Framework.Graphics;
-using osu.Framework.Graphics.Textures;
 using osu.Game.Beatmaps.ControlPoints;
 using osu.Game.Graphics.Containers;
 using osu.Game.Rulesets.Judgements;
@@ -39,7 +38,7 @@ namespace osu.Game.Rulesets.Taiko.UI
         }
 
         [BackgroundDependencyLoader(true)]
-        private void load(TextureStore textures, GameplayState gameplayState)
+        private void load(GameplayState gameplayState)
         {
             InternalChildren = new[]
             {
diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs
index 677aaf6f78..6ec14e6351 100644
--- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs
+++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs
@@ -117,7 +117,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
                 Assert.AreEqual(string.Empty, metadata.Source);
                 Assert.AreEqual("MBC7 Unisphere 地球ヤバイEP Chikyu Yabai", metadata.Tags);
                 Assert.AreEqual(557821, beatmapInfo.OnlineID);
-                Assert.AreEqual(241526, beatmapInfo.BeatmapSet.OnlineID);
+                Assert.AreEqual(241526, beatmapInfo.BeatmapSet?.OnlineID);
             }
         }
 
diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs
index 9ac7838821..3d4b05b52b 100644
--- a/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs
+++ b/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs
@@ -128,7 +128,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
                 {
                     MD5Hash = md5Hash,
                     Ruleset = new OsuRuleset().RulesetInfo,
-                    BaseDifficulty = new BeatmapDifficulty()
+                    Difficulty = new BeatmapDifficulty()
                 }
             });
         }
diff --git a/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs
index bfd6ff0314..06ed638e0a 100644
--- a/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs
+++ b/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs
@@ -31,7 +31,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
         {
             var beatmap = decodeAsJson(normal);
             var meta = beatmap.BeatmapInfo.Metadata;
-            Assert.AreEqual(241526, beatmap.BeatmapInfo.BeatmapSet.OnlineID);
+            Assert.AreEqual(241526, beatmap.BeatmapInfo.BeatmapSet?.OnlineID);
             Assert.AreEqual("Soleily", meta.Artist);
             Assert.AreEqual("Soleily", meta.ArtistUnicode);
             Assert.AreEqual("03. Renatus - Soleily 192kbps.mp3", meta.AudioFile);
diff --git a/osu.Game.Tests/Beatmaps/IO/BeatmapImportHelper.cs b/osu.Game.Tests/Beatmaps/IO/BeatmapImportHelper.cs
new file mode 100644
index 0000000000..44f6943871
--- /dev/null
+++ b/osu.Game.Tests/Beatmaps/IO/BeatmapImportHelper.cs
@@ -0,0 +1,86 @@
+// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Diagnostics;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+using NUnit.Framework;
+using osu.Framework.Allocation;
+using osu.Game.Beatmaps;
+using osu.Game.Database;
+using osu.Game.Tests.Database;
+using osu.Game.Tests.Resources;
+
+namespace osu.Game.Tests.Beatmaps.IO
+{
+    public static class BeatmapImportHelper
+    {
+        public static async Task<BeatmapSetInfo> LoadQuickOszIntoOsu(OsuGameBase osu)
+        {
+            string temp = TestResources.GetQuickTestBeatmapForImport();
+
+            var manager = osu.Dependencies.Get<BeatmapManager>();
+
+            var importedSet = await manager.Import(new ImportTask(temp)).ConfigureAwait(false);
+
+            Debug.Assert(importedSet != null);
+
+            ensureLoaded(osu);
+
+            waitForOrAssert(() => !File.Exists(temp), "Temporary file still exists after standard import", 5000);
+
+            return manager.GetAllUsableBeatmapSets().Find(beatmapSet => beatmapSet.ID == importedSet.ID);
+        }
+
+        public static async Task<BeatmapSetInfo> LoadOszIntoOsu(OsuGameBase osu, string path = null, bool virtualTrack = false)
+        {
+            string temp = path ?? TestResources.GetTestBeatmapForImport(virtualTrack);
+
+            var manager = osu.Dependencies.Get<BeatmapManager>();
+
+            var importedSet = await manager.Import(new ImportTask(temp)).ConfigureAwait(false);
+
+            Debug.Assert(importedSet != null);
+
+            ensureLoaded(osu);
+
+            waitForOrAssert(() => !File.Exists(temp), "Temporary file still exists after standard import", 5000);
+
+            return manager.GetAllUsableBeatmapSets().Find(beatmapSet => beatmapSet.ID == importedSet.ID);
+        }
+
+        private static void ensureLoaded(OsuGameBase osu, int timeout = 60000)
+        {
+            var realmContextFactory = osu.Dependencies.Get<RealmContextFactory>();
+
+            using (var realm = realmContextFactory.CreateContext())
+                BeatmapImporterTests.EnsureLoaded(realm, timeout);
+
+            // TODO: add back some extra checks outside of the realm ones?
+            // var set = queryBeatmapSets().First();
+            // foreach (BeatmapInfo b in set.Beatmaps)
+            //     Assert.IsTrue(set.Beatmaps.Any(c => c.OnlineID == b.OnlineID));
+            // Assert.IsTrue(set.Beatmaps.Count > 0);
+            // var beatmap = store.GetWorkingBeatmap(set.Beatmaps.First(b => b.RulesetID == 0))?.Beatmap;
+            // Assert.IsTrue(beatmap?.HitObjects.Any() == true);
+            // beatmap = store.GetWorkingBeatmap(set.Beatmaps.First(b => b.RulesetID == 1))?.Beatmap;
+            // Assert.IsTrue(beatmap?.HitObjects.Any() == true);
+            // beatmap = store.GetWorkingBeatmap(set.Beatmaps.First(b => b.RulesetID == 2))?.Beatmap;
+            // Assert.IsTrue(beatmap?.HitObjects.Any() == true);
+            // beatmap = store.GetWorkingBeatmap(set.Beatmaps.First(b => b.RulesetID == 3))?.Beatmap;
+            // Assert.IsTrue(beatmap?.HitObjects.Any() == true);
+        }
+
+        private static void waitForOrAssert(Func<bool> result, string failureMessage, int timeout = 60000)
+        {
+            Task task = Task.Run(() =>
+            {
+                while (!result()) Thread.Sleep(200);
+            });
+
+            Assert.IsTrue(task.Wait(timeout), failureMessage);
+        }
+    }
+}
diff --git a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs
deleted file mode 100644
index c02141bf9f..0000000000
--- a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs
+++ /dev/null
@@ -1,1106 +0,0 @@
-// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using System;
-using System.IO;
-using System.Collections.Generic;
-using System.Linq;
-using System.Threading;
-using System.Threading.Tasks;
-using NUnit.Framework;
-using osu.Framework.Platform;
-using osu.Game.IPC;
-using osu.Framework.Allocation;
-using osu.Framework.Extensions;
-using osu.Framework.Extensions.ObjectExtensions;
-using osu.Framework.Logging;
-using osu.Game.Beatmaps;
-using osu.Game.Database;
-using osu.Game.Extensions;
-using osu.Game.IO;
-using osu.Game.Online.API.Requests.Responses;
-using osu.Game.Overlays.Notifications;
-using osu.Game.Rulesets.Osu;
-using osu.Game.Rulesets.Osu.Objects;
-using osu.Game.Scoring;
-using osu.Game.Tests.Resources;
-using osu.Game.Tests.Scores.IO;
-using SharpCompress.Archives;
-using SharpCompress.Archives.Zip;
-using SharpCompress.Common;
-using SharpCompress.Writers.Zip;
-using FileInfo = System.IO.FileInfo;
-
-namespace osu.Game.Tests.Beatmaps.IO
-{
-    [TestFixture]
-    public class ImportBeatmapTest : ImportTest
-    {
-        [Test]
-        public async Task TestImportWhenClosed()
-        {
-            // unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here.
-            using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
-            {
-                try
-                {
-                    await LoadOszIntoOsu(LoadOsuIntoHost(host));
-                }
-                finally
-                {
-                    host.Exit();
-                }
-            }
-        }
-
-        [Test]
-        public async Task TestImportThenDelete()
-        {
-            // unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here.
-            using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
-            {
-                try
-                {
-                    var osu = LoadOsuIntoHost(host);
-
-                    var imported = await LoadOszIntoOsu(osu);
-
-                    deleteBeatmapSet(imported, osu);
-                }
-                finally
-                {
-                    host.Exit();
-                }
-            }
-        }
-
-        [Test]
-        public async Task TestImportThenDeleteFromStream()
-        {
-            // unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here.
-            using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
-            {
-                try
-                {
-                    var osu = LoadOsuIntoHost(host);
-
-                    string tempPath = TestResources.GetTestBeatmapForImport();
-
-                    var manager = osu.Dependencies.Get<BeatmapManager>();
-
-                    ILive<BeatmapSetInfo> importedSet;
-
-                    using (var stream = File.OpenRead(tempPath))
-                    {
-                        importedSet = await manager.Import(new ImportTask(stream, Path.GetFileName(tempPath)));
-                        await ensureLoaded(osu);
-                    }
-
-                    Assert.IsTrue(File.Exists(tempPath), "Stream source file somehow went missing");
-                    File.Delete(tempPath);
-
-                    var imported = manager.GetAllUsableBeatmapSets().Find(beatmapSet => beatmapSet.ID == importedSet.Value.ID);
-
-                    deleteBeatmapSet(imported, osu);
-                }
-                finally
-                {
-                    host.Exit();
-                }
-            }
-        }
-
-        [Test]
-        public async Task TestImportThenImport()
-        {
-            // unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here.
-            using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
-            {
-                try
-                {
-                    var osu = LoadOsuIntoHost(host);
-
-                    var imported = await LoadOszIntoOsu(osu);
-                    var importedSecondTime = await LoadOszIntoOsu(osu);
-
-                    // check the newly "imported" beatmap is actually just the restored previous import. since it matches hash.
-                    Assert.IsTrue(imported.ID == importedSecondTime.ID);
-                    Assert.IsTrue(imported.Beatmaps.First().ID == importedSecondTime.Beatmaps.First().ID);
-
-                    checkBeatmapSetCount(osu, 1);
-                    checkSingleReferencedFileCount(osu, 18);
-                }
-                finally
-                {
-                    host.Exit();
-                }
-            }
-        }
-
-        [Test]
-        public async Task TestImportThenImportWithReZip()
-        {
-            using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
-            {
-                try
-                {
-                    var osu = LoadOsuIntoHost(host);
-
-                    string temp = TestResources.GetTestBeatmapForImport();
-
-                    string extractedFolder = $"{temp}_extracted";
-                    Directory.CreateDirectory(extractedFolder);
-
-                    try
-                    {
-                        var imported = await LoadOszIntoOsu(osu);
-
-                        string hashBefore = hashFile(temp);
-
-                        using (var zip = ZipArchive.Open(temp))
-                            zip.WriteToDirectory(extractedFolder);
-
-                        using (var zip = ZipArchive.Create())
-                        {
-                            zip.AddAllFromDirectory(extractedFolder);
-                            zip.SaveTo(temp, new ZipWriterOptions(CompressionType.Deflate));
-                        }
-
-                        // zip files differ because different compression or encoder.
-                        Assert.AreNotEqual(hashBefore, hashFile(temp));
-
-                        var importedSecondTime = await osu.Dependencies.Get<BeatmapManager>().Import(new ImportTask(temp));
-
-                        await ensureLoaded(osu);
-
-                        // but contents doesn't, so existing should still be used.
-                        Assert.IsTrue(imported.ID == importedSecondTime.Value.ID);
-                        Assert.IsTrue(imported.Beatmaps.First().ID == importedSecondTime.Value.Beatmaps.First().ID);
-                    }
-                    finally
-                    {
-                        Directory.Delete(extractedFolder, true);
-                    }
-                }
-                finally
-                {
-                    host.Exit();
-                }
-            }
-        }
-
-        [Test]
-        public async Task TestImportThenImportWithChangedHashedFile()
-        {
-            using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
-            {
-                try
-                {
-                    var osu = LoadOsuIntoHost(host);
-
-                    string temp = TestResources.GetTestBeatmapForImport();
-
-                    string extractedFolder = $"{temp}_extracted";
-                    Directory.CreateDirectory(extractedFolder);
-
-                    try
-                    {
-                        var imported = await LoadOszIntoOsu(osu);
-
-                        await createScoreForBeatmap(osu, imported.Beatmaps.First());
-
-                        using (var zip = ZipArchive.Open(temp))
-                            zip.WriteToDirectory(extractedFolder);
-
-                        // arbitrary write to hashed file
-                        // this triggers the special BeatmapManager.PreImport deletion/replacement flow.
-                        using (var sw = new FileInfo(Directory.GetFiles(extractedFolder, "*.osu").First()).AppendText())
-                            await sw.WriteLineAsync("// changed");
-
-                        using (var zip = ZipArchive.Create())
-                        {
-                            zip.AddAllFromDirectory(extractedFolder);
-                            zip.SaveTo(temp, new ZipWriterOptions(CompressionType.Deflate));
-                        }
-
-                        var importedSecondTime = await osu.Dependencies.Get<BeatmapManager>().Import(new ImportTask(temp));
-
-                        await ensureLoaded(osu);
-
-                        // check the newly "imported" beatmap is not the original.
-                        Assert.IsTrue(imported.ID != importedSecondTime.Value.ID);
-                        Assert.IsTrue(imported.Beatmaps.First().ID != importedSecondTime.Value.Beatmaps.First().ID);
-                    }
-                    finally
-                    {
-                        Directory.Delete(extractedFolder, true);
-                    }
-                }
-                finally
-                {
-                    host.Exit();
-                }
-            }
-        }
-
-        [Test]
-        [Ignore("intentionally broken by import optimisations")]
-        public async Task TestImportThenImportWithChangedFile()
-        {
-            using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
-            {
-                try
-                {
-                    var osu = LoadOsuIntoHost(host);
-
-                    string temp = TestResources.GetTestBeatmapForImport();
-
-                    string extractedFolder = $"{temp}_extracted";
-                    Directory.CreateDirectory(extractedFolder);
-
-                    try
-                    {
-                        var imported = await LoadOszIntoOsu(osu);
-
-                        using (var zip = ZipArchive.Open(temp))
-                            zip.WriteToDirectory(extractedFolder);
-
-                        // arbitrary write to non-hashed file
-                        using (var sw = new FileInfo(Directory.GetFiles(extractedFolder, "*.mp3").First()).AppendText())
-                            await sw.WriteLineAsync("text");
-
-                        using (var zip = ZipArchive.Create())
-                        {
-                            zip.AddAllFromDirectory(extractedFolder);
-                            zip.SaveTo(temp, new ZipWriterOptions(CompressionType.Deflate));
-                        }
-
-                        var importedSecondTime = await osu.Dependencies.Get<BeatmapManager>().Import(new ImportTask(temp));
-
-                        await ensureLoaded(osu);
-
-                        // check the newly "imported" beatmap is not the original.
-                        Assert.IsTrue(imported.ID != importedSecondTime.Value.ID);
-                        Assert.IsTrue(imported.Beatmaps.First().ID != importedSecondTime.Value.Beatmaps.First().ID);
-                    }
-                    finally
-                    {
-                        Directory.Delete(extractedFolder, true);
-                    }
-                }
-                finally
-                {
-                    host.Exit();
-                }
-            }
-        }
-
-        [Test]
-        public async Task TestImportThenImportWithDifferentFilename()
-        {
-            using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
-            {
-                try
-                {
-                    var osu = LoadOsuIntoHost(host);
-
-                    string temp = TestResources.GetTestBeatmapForImport();
-
-                    string extractedFolder = $"{temp}_extracted";
-                    Directory.CreateDirectory(extractedFolder);
-
-                    try
-                    {
-                        var imported = await LoadOszIntoOsu(osu);
-
-                        using (var zip = ZipArchive.Open(temp))
-                            zip.WriteToDirectory(extractedFolder);
-
-                        // change filename
-                        var firstFile = new FileInfo(Directory.GetFiles(extractedFolder).First());
-                        firstFile.MoveTo(Path.Combine(firstFile.DirectoryName.AsNonNull(), $"{firstFile.Name}-changed{firstFile.Extension}"));
-
-                        using (var zip = ZipArchive.Create())
-                        {
-                            zip.AddAllFromDirectory(extractedFolder);
-                            zip.SaveTo(temp, new ZipWriterOptions(CompressionType.Deflate));
-                        }
-
-                        var importedSecondTime = await osu.Dependencies.Get<BeatmapManager>().Import(new ImportTask(temp));
-
-                        await ensureLoaded(osu);
-
-                        // check the newly "imported" beatmap is not the original.
-                        Assert.IsTrue(imported.ID != importedSecondTime.Value.ID);
-                        Assert.IsTrue(imported.Beatmaps.First().ID != importedSecondTime.Value.Beatmaps.First().ID);
-                    }
-                    finally
-                    {
-                        Directory.Delete(extractedFolder, true);
-                    }
-                }
-                finally
-                {
-                    host.Exit();
-                }
-            }
-        }
-
-        [Test]
-        [Ignore("intentionally broken by import optimisations")]
-        public async Task TestImportCorruptThenImport()
-        {
-            // unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here.
-            using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
-            {
-                try
-                {
-                    var osu = LoadOsuIntoHost(host);
-
-                    var imported = await LoadOszIntoOsu(osu);
-
-                    var firstFile = imported.Files.First();
-
-                    var files = osu.Dependencies.Get<FileStore>();
-
-                    long originalLength;
-                    using (var stream = files.Storage.GetStream(firstFile.FileInfo.GetStoragePath()))
-                        originalLength = stream.Length;
-
-                    using (var stream = files.Storage.GetStream(firstFile.FileInfo.GetStoragePath(), FileAccess.Write, FileMode.Create))
-                        stream.WriteByte(0);
-
-                    var importedSecondTime = await LoadOszIntoOsu(osu);
-
-                    using (var stream = files.Storage.GetStream(firstFile.FileInfo.GetStoragePath()))
-                        Assert.AreEqual(stream.Length, originalLength, "Corruption was not fixed on second import");
-
-                    // check the newly "imported" beatmap is actually just the restored previous import. since it matches hash.
-                    Assert.IsTrue(imported.ID == importedSecondTime.ID);
-                    Assert.IsTrue(imported.Beatmaps.First().ID == importedSecondTime.Beatmaps.First().ID);
-
-                    checkBeatmapSetCount(osu, 1);
-                    checkSingleReferencedFileCount(osu, 18);
-                }
-                finally
-                {
-                    host.Exit();
-                }
-            }
-        }
-
-        [Test]
-        public async Task TestModelCreationFailureDoesntReturn()
-        {
-            using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
-            {
-                try
-                {
-                    var osu = LoadOsuIntoHost(host);
-                    var importer = osu.Dependencies.Get<BeatmapManager>();
-
-                    var progressNotification = new ImportProgressNotification();
-
-                    var zipStream = new MemoryStream();
-
-                    using (var zip = ZipArchive.Create())
-                        zip.SaveTo(zipStream, new ZipWriterOptions(CompressionType.Deflate));
-
-                    var imported = await importer.Import(
-                        progressNotification,
-                        new ImportTask(zipStream, string.Empty)
-                    );
-
-                    checkBeatmapSetCount(osu, 0);
-                    checkBeatmapCount(osu, 0);
-
-                    Assert.IsEmpty(imported);
-                    Assert.AreEqual(ProgressNotificationState.Cancelled, progressNotification.State);
-                }
-                finally
-                {
-                    host.Exit();
-                }
-            }
-        }
-
-        [Test]
-        public async Task TestRollbackOnFailure()
-        {
-            // unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here.
-            using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
-            {
-                try
-                {
-                    int itemAddRemoveFireCount = 0;
-                    int loggedExceptionCount = 0;
-
-                    Logger.NewEntry += l =>
-                    {
-                        if (l.Target == LoggingTarget.Database && l.Exception != null)
-                            Interlocked.Increment(ref loggedExceptionCount);
-                    };
-
-                    var osu = LoadOsuIntoHost(host);
-                    var manager = osu.Dependencies.Get<BeatmapManager>();
-
-                    // ReSharper disable once AccessToModifiedClosure
-                    manager.ItemUpdated += _ => Interlocked.Increment(ref itemAddRemoveFireCount);
-                    manager.ItemRemoved += _ => Interlocked.Increment(ref itemAddRemoveFireCount);
-
-                    var imported = await LoadOszIntoOsu(osu);
-
-                    Assert.AreEqual(0, itemAddRemoveFireCount -= 1);
-
-                    imported.Hash += "-changed";
-                    manager.Update(imported);
-
-                    Assert.AreEqual(0, itemAddRemoveFireCount -= 1);
-
-                    checkBeatmapSetCount(osu, 1);
-                    checkBeatmapCount(osu, 12);
-                    checkSingleReferencedFileCount(osu, 18);
-
-                    string brokenTempFilename = TestResources.GetTestBeatmapForImport();
-
-                    MemoryStream brokenOsu = new MemoryStream();
-                    MemoryStream brokenOsz = new MemoryStream(await File.ReadAllBytesAsync(brokenTempFilename));
-
-                    File.Delete(brokenTempFilename);
-
-                    using (var outStream = File.Open(brokenTempFilename, FileMode.CreateNew))
-                    using (var zip = ZipArchive.Open(brokenOsz))
-                    {
-                        zip.AddEntry("broken.osu", brokenOsu, false);
-                        zip.SaveTo(outStream, CompressionType.Deflate);
-                    }
-
-                    // this will trigger purging of the existing beatmap (online set id match) but should rollback due to broken osu.
-                    try
-                    {
-                        await manager.Import(new ImportTask(brokenTempFilename));
-                    }
-                    catch
-                    {
-                    }
-
-                    // no events should be fired in the case of a rollback.
-                    Assert.AreEqual(0, itemAddRemoveFireCount);
-
-                    checkBeatmapSetCount(osu, 1);
-                    checkBeatmapCount(osu, 12);
-
-                    checkSingleReferencedFileCount(osu, 18);
-
-                    Assert.AreEqual(1, loggedExceptionCount);
-
-                    File.Delete(brokenTempFilename);
-                }
-                finally
-                {
-                    host.Exit();
-                }
-            }
-        }
-
-        [Test]
-        public async Task TestImportThenDeleteThenImport()
-        {
-            // unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here.
-            using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
-            {
-                try
-                {
-                    var osu = LoadOsuIntoHost(host);
-
-                    var imported = await LoadOszIntoOsu(osu);
-
-                    deleteBeatmapSet(imported, osu);
-
-                    var importedSecondTime = await LoadOszIntoOsu(osu);
-
-                    // check the newly "imported" beatmap is actually just the restored previous import. since it matches hash.
-                    Assert.IsTrue(imported.ID == importedSecondTime.ID);
-                    Assert.IsTrue(imported.Beatmaps.First().ID == importedSecondTime.Beatmaps.First().ID);
-                }
-                finally
-                {
-                    host.Exit();
-                }
-            }
-        }
-
-        [Test]
-        public async Task TestImportThenDeleteThenImportWithOnlineIDsMissing()
-        {
-            // unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here.
-            using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
-            {
-                try
-                {
-                    var osu = LoadOsuIntoHost(host);
-
-                    var imported = await LoadOszIntoOsu(osu);
-
-                    foreach (var b in imported.Beatmaps)
-                        b.OnlineID = null;
-
-                    osu.Dependencies.Get<BeatmapManager>().Update(imported);
-
-                    deleteBeatmapSet(imported, osu);
-
-                    var importedSecondTime = await LoadOszIntoOsu(osu);
-
-                    // check the newly "imported" beatmap has been reimported due to mismatch (even though hashes matched)
-                    Assert.IsTrue(imported.ID != importedSecondTime.ID);
-                    Assert.IsTrue(imported.Beatmaps.First().ID != importedSecondTime.Beatmaps.First().ID);
-                }
-                finally
-                {
-                    host.Exit();
-                }
-            }
-        }
-
-        [Test]
-        public async Task TestImportWithDuplicateBeatmapIDs()
-        {
-            // unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here.
-            using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
-            {
-                try
-                {
-                    var osu = LoadOsuIntoHost(host);
-
-                    var metadata = new BeatmapMetadata
-                    {
-                        Artist = "SomeArtist",
-                        AuthorString = "SomeAuthor"
-                    };
-
-                    var difficulty = new BeatmapDifficulty();
-
-                    var toImport = new BeatmapSetInfo
-                    {
-                        OnlineID = 1,
-                        Metadata = metadata,
-                        Beatmaps =
-                        {
-                            new BeatmapInfo
-                            {
-                                OnlineID = 2,
-                                Metadata = metadata,
-                                BaseDifficulty = difficulty
-                            },
-                            new BeatmapInfo
-                            {
-                                OnlineID = 2,
-                                Metadata = metadata,
-                                Status = BeatmapOnlineStatus.Loved,
-                                BaseDifficulty = difficulty
-                            }
-                        }
-                    };
-
-                    var manager = osu.Dependencies.Get<BeatmapManager>();
-
-                    var imported = await manager.Import(toImport);
-
-                    Assert.NotNull(imported);
-                    Assert.AreEqual(null, imported.Value.Beatmaps[0].OnlineID);
-                    Assert.AreEqual(null, imported.Value.Beatmaps[1].OnlineID);
-                }
-                finally
-                {
-                    host.Exit();
-                }
-            }
-        }
-
-        [Test]
-        [NonParallelizable]
-        public void TestImportOverIPC()
-        {
-            using (HeadlessGameHost host = new CleanRunHeadlessGameHost(true))
-            using (HeadlessGameHost client = new CleanRunHeadlessGameHost(true))
-            {
-                try
-                {
-                    Assert.IsTrue(host.IsPrimaryInstance);
-                    Assert.IsFalse(client.IsPrimaryInstance);
-
-                    var osu = LoadOsuIntoHost(host);
-
-                    string temp = TestResources.GetTestBeatmapForImport();
-
-                    var importer = new ArchiveImportIPCChannel(client);
-                    if (!importer.ImportAsync(temp).Wait(10000))
-                        Assert.Fail(@"IPC took too long to send");
-
-                    ensureLoaded(osu).WaitSafely();
-
-                    waitForOrAssert(() => !File.Exists(temp), "Temporary still exists after IPC import", 5000);
-                }
-                finally
-                {
-                    host.Exit();
-                }
-            }
-        }
-
-        [Test]
-        public async Task TestImportWhenFileOpen()
-        {
-            using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
-            {
-                try
-                {
-                    var osu = LoadOsuIntoHost(host);
-                    string temp = TestResources.GetTestBeatmapForImport();
-                    using (File.OpenRead(temp))
-                        await osu.Dependencies.Get<BeatmapManager>().Import(temp);
-                    await ensureLoaded(osu);
-                    File.Delete(temp);
-                    Assert.IsFalse(File.Exists(temp), "We likely held a read lock on the file when we shouldn't");
-                }
-                finally
-                {
-                    host.Exit();
-                }
-            }
-        }
-
-        [Test]
-        public async Task TestImportWithDuplicateHashes()
-        {
-            using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
-            {
-                try
-                {
-                    var osu = LoadOsuIntoHost(host);
-
-                    string temp = TestResources.GetTestBeatmapForImport();
-
-                    string extractedFolder = $"{temp}_extracted";
-                    Directory.CreateDirectory(extractedFolder);
-
-                    try
-                    {
-                        using (var zip = ZipArchive.Open(temp))
-                            zip.WriteToDirectory(extractedFolder);
-
-                        using (var zip = ZipArchive.Create())
-                        {
-                            zip.AddAllFromDirectory(extractedFolder);
-                            zip.AddEntry("duplicate.osu", Directory.GetFiles(extractedFolder, "*.osu").First());
-                            zip.SaveTo(temp, new ZipWriterOptions(CompressionType.Deflate));
-                        }
-
-                        await osu.Dependencies.Get<BeatmapManager>().Import(temp);
-
-                        await ensureLoaded(osu);
-                    }
-                    finally
-                    {
-                        Directory.Delete(extractedFolder, true);
-                    }
-                }
-                finally
-                {
-                    host.Exit();
-                }
-            }
-        }
-
-        [Test]
-        public async Task TestImportNestedStructure()
-        {
-            using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
-            {
-                try
-                {
-                    var osu = LoadOsuIntoHost(host);
-
-                    string temp = TestResources.GetTestBeatmapForImport();
-
-                    string extractedFolder = $"{temp}_extracted";
-                    string subfolder = Path.Combine(extractedFolder, "subfolder");
-
-                    Directory.CreateDirectory(subfolder);
-
-                    try
-                    {
-                        using (var zip = ZipArchive.Open(temp))
-                            zip.WriteToDirectory(subfolder);
-
-                        using (var zip = ZipArchive.Create())
-                        {
-                            zip.AddAllFromDirectory(extractedFolder);
-                            zip.SaveTo(temp, new ZipWriterOptions(CompressionType.Deflate));
-                        }
-
-                        var imported = await osu.Dependencies.Get<BeatmapManager>().Import(new ImportTask(temp));
-
-                        await ensureLoaded(osu);
-
-                        Assert.IsFalse(imported.Value.Files.Any(f => f.Filename.Contains("subfolder")), "Files contain common subfolder");
-                    }
-                    finally
-                    {
-                        Directory.Delete(extractedFolder, true);
-                    }
-                }
-                finally
-                {
-                    host.Exit();
-                }
-            }
-        }
-
-        [Test]
-        public async Task TestImportWithIgnoredDirectoryInArchive()
-        {
-            using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
-            {
-                try
-                {
-                    var osu = LoadOsuIntoHost(host);
-
-                    string temp = TestResources.GetTestBeatmapForImport();
-
-                    string extractedFolder = $"{temp}_extracted";
-                    string dataFolder = Path.Combine(extractedFolder, "actual_data");
-                    string resourceForkFolder = Path.Combine(extractedFolder, "__MACOSX");
-                    string resourceForkFilePath = Path.Combine(resourceForkFolder, ".extracted");
-
-                    Directory.CreateDirectory(dataFolder);
-                    Directory.CreateDirectory(resourceForkFolder);
-
-                    using (var resourceForkFile = File.CreateText(resourceForkFilePath))
-                    {
-                        await resourceForkFile.WriteLineAsync("adding content so that it's not empty");
-                    }
-
-                    try
-                    {
-                        using (var zip = ZipArchive.Open(temp))
-                            zip.WriteToDirectory(dataFolder);
-
-                        using (var zip = ZipArchive.Create())
-                        {
-                            zip.AddAllFromDirectory(extractedFolder);
-                            zip.SaveTo(temp, new ZipWriterOptions(CompressionType.Deflate));
-                        }
-
-                        var imported = await osu.Dependencies.Get<BeatmapManager>().Import(new ImportTask(temp));
-
-                        await ensureLoaded(osu);
-
-                        Assert.IsFalse(imported.Value.Files.Any(f => f.Filename.Contains("__MACOSX")), "Files contain resource fork folder, which should be ignored");
-                        Assert.IsFalse(imported.Value.Files.Any(f => f.Filename.Contains("actual_data")), "Files contain common subfolder");
-                    }
-                    finally
-                    {
-                        Directory.Delete(extractedFolder, true);
-                    }
-                }
-                finally
-                {
-                    host.Exit();
-                }
-            }
-        }
-
-        [Test]
-        public void TestUpdateBeatmapInfo()
-        {
-            using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
-            {
-                try
-                {
-                    var osu = LoadOsuIntoHost(host);
-                    var manager = osu.Dependencies.Get<BeatmapManager>();
-
-                    string temp = TestResources.GetTestBeatmapForImport();
-
-                    osu.Dependencies.Get<BeatmapManager>().Import(temp).WaitSafely();
-
-                    // Update via the beatmap, not the beatmap info, to ensure correct linking
-                    BeatmapSetInfo setToUpdate = manager.GetAllUsableBeatmapSets()[0];
-                    Beatmap beatmapToUpdate = (Beatmap)manager.GetWorkingBeatmap(setToUpdate.Beatmaps.First(b => b.RulesetID == 0)).Beatmap;
-                    beatmapToUpdate.BeatmapInfo.DifficultyName = "updated";
-
-                    manager.Update(setToUpdate);
-
-                    BeatmapInfo updatedInfo = manager.QueryBeatmap(b => b.ID == beatmapToUpdate.BeatmapInfo.ID);
-                    Assert.That(updatedInfo.DifficultyName, Is.EqualTo("updated"));
-                }
-                finally
-                {
-                    host.Exit();
-                }
-            }
-        }
-
-        [Test]
-        public void TestUpdateBeatmapFile()
-        {
-            using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
-            {
-                try
-                {
-                    var osu = LoadOsuIntoHost(host);
-                    var manager = osu.Dependencies.Get<BeatmapManager>();
-
-                    string temp = TestResources.GetTestBeatmapForImport();
-
-                    osu.Dependencies.Get<BeatmapManager>().Import(temp).WaitSafely();
-
-                    BeatmapSetInfo setToUpdate = manager.GetAllUsableBeatmapSets()[0];
-
-                    var beatmapInfo = setToUpdate.Beatmaps.First(b => b.RulesetID == 0);
-                    Beatmap beatmapToUpdate = (Beatmap)manager.GetWorkingBeatmap(setToUpdate.Beatmaps.First(b => b.RulesetID == 0)).Beatmap;
-                    BeatmapSetFileInfo fileToUpdate = setToUpdate.Files.First(f => beatmapToUpdate.BeatmapInfo.Path.Contains(f.Filename));
-
-                    string oldMd5Hash = beatmapToUpdate.BeatmapInfo.MD5Hash;
-
-                    beatmapToUpdate.HitObjects.Clear();
-                    beatmapToUpdate.HitObjects.Add(new HitCircle { StartTime = 5000 });
-
-                    manager.Save(beatmapInfo, beatmapToUpdate);
-
-                    // Check that the old file reference has been removed
-                    Assert.That(manager.QueryBeatmapSet(s => s.ID == setToUpdate.ID).Files.All(f => f.ID != fileToUpdate.ID));
-
-                    // Check that the new file is referenced correctly by attempting a retrieval
-                    Beatmap updatedBeatmap = (Beatmap)manager.GetWorkingBeatmap(manager.QueryBeatmap(b => b.ID == beatmapToUpdate.BeatmapInfo.ID)).Beatmap;
-                    Assert.That(updatedBeatmap.HitObjects.Count, Is.EqualTo(1));
-                    Assert.That(updatedBeatmap.HitObjects[0].StartTime, Is.EqualTo(5000));
-                    Assert.That(updatedBeatmap.BeatmapInfo.MD5Hash, Is.Not.EqualTo(oldMd5Hash));
-                }
-                finally
-                {
-                    host.Exit();
-                }
-            }
-        }
-
-        // TODO: needs to be pulled across to realm implementation when this file is nuked.
-        [Test]
-        public void TestSaveRemovesInvalidCharactersFromPath()
-        {
-            // unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here.
-            using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
-            {
-                try
-                {
-                    var osu = LoadOsuIntoHost(host);
-
-                    var manager = osu.Dependencies.Get<BeatmapManager>();
-
-                    var working = manager.CreateNew(new OsuRuleset().RulesetInfo, APIUser.SYSTEM_USER);
-
-                    var beatmap = working.Beatmap;
-
-                    beatmap.BeatmapInfo.DifficultyName = "difficulty";
-                    beatmap.BeatmapInfo.Metadata = new BeatmapMetadata
-                    {
-                        Artist = "Artist/With\\Slashes",
-                        Title = "Title",
-                        AuthorString = "mapper",
-                    };
-
-                    manager.Save(beatmap.BeatmapInfo, working.Beatmap);
-
-                    Assert.AreEqual("Artist_With_Slashes - Title (mapper) [difficulty].osu", beatmap.BeatmapInfo.Path);
-                }
-                finally
-                {
-                    host.Exit();
-                }
-            }
-        }
-
-        [Test]
-        public void TestCreateNewEmptyBeatmap()
-        {
-            using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
-            {
-                try
-                {
-                    var osu = LoadOsuIntoHost(host);
-                    var manager = osu.Dependencies.Get<BeatmapManager>();
-
-                    var working = manager.CreateNew(new OsuRuleset().RulesetInfo, APIUser.SYSTEM_USER);
-
-                    manager.Save(working.BeatmapInfo, working.Beatmap);
-
-                    var retrievedSet = manager.GetAllUsableBeatmapSets()[0];
-
-                    // Check that the new file is referenced correctly by attempting a retrieval
-                    Beatmap updatedBeatmap = (Beatmap)manager.GetWorkingBeatmap(retrievedSet.Beatmaps[0]).Beatmap;
-                    Assert.That(updatedBeatmap.HitObjects.Count, Is.EqualTo(0));
-                }
-                finally
-                {
-                    host.Exit();
-                }
-            }
-        }
-
-        [Test]
-        public void TestCreateNewBeatmapWithObject()
-        {
-            using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
-            {
-                try
-                {
-                    var osu = LoadOsuIntoHost(host);
-                    var manager = osu.Dependencies.Get<BeatmapManager>();
-
-                    var working = manager.CreateNew(new OsuRuleset().RulesetInfo, APIUser.SYSTEM_USER);
-
-                    ((Beatmap)working.Beatmap).HitObjects.Add(new HitCircle { StartTime = 5000 });
-
-                    manager.Save(working.BeatmapInfo, working.Beatmap);
-
-                    var retrievedSet = manager.GetAllUsableBeatmapSets()[0];
-
-                    // Check that the new file is referenced correctly by attempting a retrieval
-                    Beatmap updatedBeatmap = (Beatmap)manager.GetWorkingBeatmap(retrievedSet.Beatmaps[0]).Beatmap;
-                    Assert.That(updatedBeatmap.HitObjects.Count, Is.EqualTo(1));
-                    Assert.That(updatedBeatmap.HitObjects[0].StartTime, Is.EqualTo(5000));
-                }
-                finally
-                {
-                    host.Exit();
-                }
-            }
-        }
-
-        public static Task<BeatmapSetInfo> LoadQuickOszIntoOsu(OsuGameBase osu) => Task.Factory.StartNew(() =>
-        {
-            string temp = TestResources.GetQuickTestBeatmapForImport();
-
-            var manager = osu.Dependencies.Get<BeatmapManager>();
-
-            var importedSet = manager.Import(new ImportTask(temp)).GetResultSafely();
-
-            ensureLoaded(osu).WaitSafely();
-
-            waitForOrAssert(() => !File.Exists(temp), "Temporary file still exists after standard import", 5000);
-
-            return manager.GetAllUsableBeatmapSets().Find(beatmapSet => beatmapSet.ID == importedSet.Value.ID);
-        }, TaskCreationOptions.LongRunning);
-
-        public static Task<BeatmapSetInfo> LoadOszIntoOsu(OsuGameBase osu, string path = null, bool virtualTrack = false) => Task.Factory.StartNew(() =>
-        {
-            string temp = path ?? TestResources.GetTestBeatmapForImport(virtualTrack);
-
-            var manager = osu.Dependencies.Get<BeatmapManager>();
-
-            var importedSet = manager.Import(new ImportTask(temp)).GetResultSafely();
-
-            ensureLoaded(osu).WaitSafely();
-
-            waitForOrAssert(() => !File.Exists(temp), "Temporary file still exists after standard import", 5000);
-
-            return manager.GetAllUsableBeatmapSets().Find(beatmapSet => beatmapSet.ID == importedSet.Value.ID);
-        }, TaskCreationOptions.LongRunning);
-
-        private void deleteBeatmapSet(BeatmapSetInfo imported, OsuGameBase osu)
-        {
-            var manager = osu.Dependencies.Get<BeatmapManager>();
-            manager.Delete(imported);
-
-            checkBeatmapSetCount(osu, 0);
-            checkBeatmapSetCount(osu, 1, true);
-            checkSingleReferencedFileCount(osu, 0);
-
-            Assert.IsTrue(manager.QueryBeatmapSets(_ => true).First().DeletePending);
-        }
-
-        private static Task createScoreForBeatmap(OsuGameBase osu, BeatmapInfo beatmapInfo)
-        {
-            return ImportScoreTest.LoadScoreIntoOsu(osu, new ScoreInfo
-            {
-                OnlineID = 2,
-                BeatmapInfo = beatmapInfo,
-                BeatmapInfoID = beatmapInfo.ID
-            }, new ImportScoreTest.TestArchiveReader());
-        }
-
-        private static void checkBeatmapSetCount(OsuGameBase osu, int expected, bool includeDeletePending = false)
-        {
-            var manager = osu.Dependencies.Get<BeatmapManager>();
-
-            Assert.AreEqual(expected, includeDeletePending
-                ? manager.QueryBeatmapSets(_ => true).ToList().Count
-                : manager.GetAllUsableBeatmapSets().Count);
-        }
-
-        private static string hashFile(string filename)
-        {
-            using (var s = File.OpenRead(filename))
-                return s.ComputeMD5Hash();
-        }
-
-        private static void checkBeatmapCount(OsuGameBase osu, int expected)
-        {
-            Assert.AreEqual(expected, osu.Dependencies.Get<BeatmapManager>().QueryBeatmaps(_ => true).ToList().Count);
-        }
-
-        private static void checkSingleReferencedFileCount(OsuGameBase osu, int expected)
-        {
-            Assert.AreEqual(expected, osu.Dependencies.Get<DatabaseContextFactory>().Get().FileInfo.Count(f => f.ReferenceCount == 1));
-        }
-
-        private static Task ensureLoaded(OsuGameBase osu, int timeout = 60000) => Task.Factory.StartNew(() =>
-        {
-            IEnumerable<BeatmapSetInfo> resultSets = null;
-            var store = osu.Dependencies.Get<BeatmapManager>();
-            waitForOrAssert(() => (resultSets = store.QueryBeatmapSets(s => s.OnlineID == 241526)).Any(),
-                @"BeatmapSet did not import to the database in allocated time.", timeout);
-
-            // ensure we were stored to beatmap database backing...
-            Assert.IsTrue(resultSets.Count() == 1, $@"Incorrect result count found ({resultSets.Count()} but should be 1).");
-            IEnumerable<BeatmapInfo> queryBeatmaps() => store.QueryBeatmaps(s => s.BeatmapSet.OnlineID == 241526 && s.BaseDifficultyID > 0);
-            IEnumerable<BeatmapSetInfo> queryBeatmapSets() => store.QueryBeatmapSets(s => s.OnlineID == 241526);
-
-            // if we don't re-check here, the set will be inserted but the beatmaps won't be present yet.
-            waitForOrAssert(() => queryBeatmaps().Count() == 12,
-                @"Beatmaps did not import to the database in allocated time", timeout);
-            waitForOrAssert(() => queryBeatmapSets().Count() == 1,
-                @"BeatmapSet did not import to the database in allocated time", timeout);
-            int countBeatmapSetBeatmaps = 0;
-            int countBeatmaps = 0;
-            waitForOrAssert(() =>
-                    (countBeatmapSetBeatmaps = queryBeatmapSets().First().Beatmaps.Count) ==
-                    (countBeatmaps = queryBeatmaps().Count()),
-                $@"Incorrect database beatmap count post-import ({countBeatmaps} but should be {countBeatmapSetBeatmaps}).", timeout);
-
-            var set = queryBeatmapSets().First();
-            foreach (BeatmapInfo b in set.Beatmaps)
-                Assert.IsTrue(set.Beatmaps.Any(c => c.OnlineID == b.OnlineID));
-            Assert.IsTrue(set.Beatmaps.Count > 0);
-            var beatmap = store.GetWorkingBeatmap(set.Beatmaps.First(b => b.RulesetID == 0))?.Beatmap;
-            Assert.IsTrue(beatmap?.HitObjects.Any() == true);
-            beatmap = store.GetWorkingBeatmap(set.Beatmaps.First(b => b.RulesetID == 1))?.Beatmap;
-            Assert.IsTrue(beatmap?.HitObjects.Any() == true);
-            beatmap = store.GetWorkingBeatmap(set.Beatmaps.First(b => b.RulesetID == 2))?.Beatmap;
-            Assert.IsTrue(beatmap?.HitObjects.Any() == true);
-            beatmap = store.GetWorkingBeatmap(set.Beatmaps.First(b => b.RulesetID == 3))?.Beatmap;
-            Assert.IsTrue(beatmap?.HitObjects.Any() == true);
-        }, TaskCreationOptions.LongRunning);
-
-        private static void waitForOrAssert(Func<bool> result, string failureMessage, int timeout = 60000)
-        {
-            Task task = Task.Factory.StartNew(() =>
-            {
-                while (!result()) Thread.Sleep(200);
-            }, TaskCreationOptions.LongRunning);
-
-            Assert.IsTrue(task.Wait(timeout), failureMessage);
-        }
-    }
-}
diff --git a/osu.Game.Tests/Beatmaps/IO/OszArchiveReaderTest.cs b/osu.Game.Tests/Beatmaps/IO/OszArchiveReaderTest.cs
index b2ab1eeaa6..810ea5dbd0 100644
--- a/osu.Game.Tests/Beatmaps/IO/OszArchiveReaderTest.cs
+++ b/osu.Game.Tests/Beatmaps/IO/OszArchiveReaderTest.cs
@@ -56,7 +56,7 @@ namespace osu.Game.Tests.Beatmaps.IO
 
                 var meta = beatmap.Metadata;
 
-                Assert.AreEqual(241526, beatmap.BeatmapInfo.BeatmapSet.OnlineID);
+                Assert.AreEqual(241526, beatmap.BeatmapInfo.BeatmapSet?.OnlineID);
                 Assert.AreEqual("Soleily", meta.Artist);
                 Assert.AreEqual("Soleily", meta.ArtistUnicode);
                 Assert.AreEqual("03. Renatus - Soleily 192kbps.mp3", meta.AudioFile);
diff --git a/osu.Game.Tests/Beatmaps/TestSceneBeatmapDifficultyCache.cs b/osu.Game.Tests/Beatmaps/TestSceneBeatmapDifficultyCache.cs
index 26ab8808b9..f3456cf8e4 100644
--- a/osu.Game.Tests/Beatmaps/TestSceneBeatmapDifficultyCache.cs
+++ b/osu.Game.Tests/Beatmaps/TestSceneBeatmapDifficultyCache.cs
@@ -24,6 +24,8 @@ namespace osu.Game.Tests.Beatmaps
     {
         public const double BASE_STARS = 5.55;
 
+        private static readonly Guid guid = Guid.NewGuid();
+
         private BeatmapSetInfo importedSet;
 
         private TestBeatmapDifficultyCache difficultyCache;
@@ -33,7 +35,7 @@ namespace osu.Game.Tests.Beatmaps
         [BackgroundDependencyLoader]
         private void load(OsuGameBase osu)
         {
-            importedSet = ImportBeatmapTest.LoadQuickOszIntoOsu(osu).GetResultSafely();
+            importedSet = BeatmapImportHelper.LoadQuickOszIntoOsu(osu).GetResultSafely();
         }
 
         [SetUpSteps]
@@ -98,8 +100,8 @@ namespace osu.Game.Tests.Beatmaps
         [Test]
         public void TestKeyEqualsWithDifferentModInstances()
         {
-            var key1 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = 1234 }, new RulesetInfo { OnlineID = 0 }, new Mod[] { new OsuModHardRock(), new OsuModHidden() });
-            var key2 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = 1234 }, new RulesetInfo { OnlineID = 0 }, new Mod[] { new OsuModHardRock(), new OsuModHidden() });
+            var key1 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = guid }, new RulesetInfo { OnlineID = 0 }, new Mod[] { new OsuModHardRock(), new OsuModHidden() });
+            var key2 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = guid }, new RulesetInfo { OnlineID = 0 }, new Mod[] { new OsuModHardRock(), new OsuModHidden() });
 
             Assert.That(key1, Is.EqualTo(key2));
             Assert.That(key1.GetHashCode(), Is.EqualTo(key2.GetHashCode()));
@@ -108,8 +110,8 @@ namespace osu.Game.Tests.Beatmaps
         [Test]
         public void TestKeyEqualsWithDifferentModOrder()
         {
-            var key1 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = 1234 }, new RulesetInfo { OnlineID = 0 }, new Mod[] { new OsuModHardRock(), new OsuModHidden() });
-            var key2 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = 1234 }, new RulesetInfo { OnlineID = 0 }, new Mod[] { new OsuModHidden(), new OsuModHardRock() });
+            var key1 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = guid }, new RulesetInfo { OnlineID = 0 }, new Mod[] { new OsuModHardRock(), new OsuModHidden() });
+            var key2 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = guid }, new RulesetInfo { OnlineID = 0 }, new Mod[] { new OsuModHidden(), new OsuModHardRock() });
 
             Assert.That(key1, Is.EqualTo(key2));
             Assert.That(key1.GetHashCode(), Is.EqualTo(key2.GetHashCode()));
@@ -118,8 +120,8 @@ namespace osu.Game.Tests.Beatmaps
         [Test]
         public void TestKeyDoesntEqualWithDifferentModSettings()
         {
-            var key1 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = 1234 }, new RulesetInfo { OnlineID = 0 }, new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.1 } } });
-            var key2 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = 1234 }, new RulesetInfo { OnlineID = 0 }, new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.9 } } });
+            var key1 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = guid }, new RulesetInfo { OnlineID = 0 }, new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.1 } } });
+            var key2 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = guid }, new RulesetInfo { OnlineID = 0 }, new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.9 } } });
 
             Assert.That(key1, Is.Not.EqualTo(key2));
             Assert.That(key1.GetHashCode(), Is.Not.EqualTo(key2.GetHashCode()));
@@ -128,8 +130,8 @@ namespace osu.Game.Tests.Beatmaps
         [Test]
         public void TestKeyEqualWithMatchingModSettings()
         {
-            var key1 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = 1234 }, new RulesetInfo { OnlineID = 0 }, new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.25 } } });
-            var key2 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = 1234 }, new RulesetInfo { OnlineID = 0 }, new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.25 } } });
+            var key1 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = guid }, new RulesetInfo { OnlineID = 0 }, new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.25 } } });
+            var key2 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = guid }, new RulesetInfo { OnlineID = 0 }, new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.25 } } });
 
             Assert.That(key1, Is.EqualTo(key2));
             Assert.That(key1.GetHashCode(), Is.EqualTo(key2.GetHashCode()));
diff --git a/osu.Game.Tests/Beatmaps/TestSceneEditorBeatmap.cs b/osu.Game.Tests/Beatmaps/TestSceneEditorBeatmap.cs
index bf5b517603..153788c2cf 100644
--- a/osu.Game.Tests/Beatmaps/TestSceneEditorBeatmap.cs
+++ b/osu.Game.Tests/Beatmaps/TestSceneEditorBeatmap.cs
@@ -7,6 +7,7 @@ using System.Linq;
 using NUnit.Framework;
 using osu.Framework.Testing;
 using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Osu;
 using osu.Game.Rulesets.Osu.Beatmaps;
 using osu.Game.Rulesets.Osu.Objects;
 using osu.Game.Screens.Edit;
@@ -30,7 +31,13 @@ namespace osu.Game.Tests.Beatmaps
 
             AddStep("add beatmap", () =>
             {
-                Child = editorBeatmap = new EditorBeatmap(new OsuBeatmap());
+                Child = editorBeatmap = new EditorBeatmap(new OsuBeatmap
+                {
+                    BeatmapInfo =
+                    {
+                        Ruleset = new OsuRuleset().RulesetInfo,
+                    },
+                });
                 editorBeatmap.HitObjectAdded += h => addedObject = h;
             });
 
@@ -49,7 +56,14 @@ namespace osu.Game.Tests.Beatmaps
             EditorBeatmap editorBeatmap = null;
             AddStep("add beatmap", () =>
             {
-                Child = editorBeatmap = new EditorBeatmap(new OsuBeatmap { HitObjects = { hitCircle } });
+                Child = editorBeatmap = new EditorBeatmap(new OsuBeatmap
+                {
+                    BeatmapInfo =
+                    {
+                        Ruleset = new OsuRuleset().RulesetInfo,
+                    },
+                    HitObjects = { hitCircle }
+                });
                 editorBeatmap.HitObjectRemoved += h => removedObject = h;
             });
             AddStep("remove hitobject", () => editorBeatmap.Remove(editorBeatmap.HitObjects.First()));
@@ -71,7 +85,14 @@ namespace osu.Game.Tests.Beatmaps
             {
                 EditorBeatmap editorBeatmap;
 
-                Child = editorBeatmap = new EditorBeatmap(new OsuBeatmap { HitObjects = { hitCircle } });
+                Child = editorBeatmap = new EditorBeatmap(new OsuBeatmap
+                {
+                    BeatmapInfo =
+                    {
+                        Ruleset = new OsuRuleset().RulesetInfo,
+                    },
+                    HitObjects = { hitCircle }
+                });
                 editorBeatmap.HitObjectUpdated += h => changedObject = h;
             });
 
@@ -91,7 +112,13 @@ namespace osu.Game.Tests.Beatmaps
 
             AddStep("add beatmap", () =>
             {
-                Child = editorBeatmap = new EditorBeatmap(new OsuBeatmap());
+                Child = editorBeatmap = new EditorBeatmap(new OsuBeatmap
+                {
+                    BeatmapInfo =
+                    {
+                        Ruleset = new OsuRuleset().RulesetInfo,
+                    },
+                });
                 editorBeatmap.HitObjectUpdated += h => changedObject = h;
             });
 
@@ -111,7 +138,14 @@ namespace osu.Game.Tests.Beatmaps
         public void TestRemovedHitObjectStartTimeChangeEvent()
         {
             var hitCircle = new HitCircle();
-            var editorBeatmap = new EditorBeatmap(new OsuBeatmap { HitObjects = { hitCircle } });
+            var editorBeatmap = new EditorBeatmap(new OsuBeatmap
+            {
+                BeatmapInfo =
+                {
+                    Ruleset = new OsuRuleset().RulesetInfo,
+                },
+                HitObjects = { hitCircle }
+            });
 
             HitObject changedObject = null;
             editorBeatmap.HitObjectUpdated += h => changedObject = h;
@@ -131,6 +165,10 @@ namespace osu.Game.Tests.Beatmaps
         {
             var editorBeatmap = new EditorBeatmap(new OsuBeatmap
             {
+                BeatmapInfo =
+                {
+                    Ruleset = new OsuRuleset().RulesetInfo,
+                },
                 HitObjects =
                 {
                     new HitCircle(),
@@ -156,6 +194,10 @@ namespace osu.Game.Tests.Beatmaps
 
             var editorBeatmap = new EditorBeatmap(new OsuBeatmap
             {
+                BeatmapInfo =
+                {
+                    Ruleset = new OsuRuleset().RulesetInfo,
+                },
                 HitObjects =
                 {
                     new HitCircle(),
@@ -185,7 +227,13 @@ namespace osu.Game.Tests.Beatmaps
             {
                 updatedObjects.Clear();
 
-                Child = editorBeatmap = new EditorBeatmap(new OsuBeatmap());
+                Child = editorBeatmap = new EditorBeatmap(new OsuBeatmap
+                {
+                    BeatmapInfo =
+                    {
+                        Ruleset = new OsuRuleset().RulesetInfo,
+                    },
+                });
 
                 for (int i = 0; i < 10; i++)
                 {
@@ -220,7 +268,13 @@ namespace osu.Game.Tests.Beatmaps
             {
                 updatedObjects.Clear();
 
-                Child = editorBeatmap = new EditorBeatmap(new OsuBeatmap());
+                Child = editorBeatmap = new EditorBeatmap(new OsuBeatmap
+                {
+                    BeatmapInfo =
+                    {
+                        Ruleset = new OsuRuleset().RulesetInfo,
+                    },
+                });
                 editorBeatmap.Add(new HitCircle());
             });
 
diff --git a/osu.Game.Tests/Beatmaps/ToStringFormattingTest.cs b/osu.Game.Tests/Beatmaps/ToStringFormattingTest.cs
index 4a7d7505ad..10cac4ed9d 100644
--- a/osu.Game.Tests/Beatmaps/ToStringFormattingTest.cs
+++ b/osu.Game.Tests/Beatmaps/ToStringFormattingTest.cs
@@ -3,7 +3,7 @@
 
 using NUnit.Framework;
 using osu.Game.Beatmaps;
-using osu.Game.Online.API.Requests.Responses;
+using osu.Game.Models;
 
 namespace osu.Game.Tests.Beatmaps
 {
@@ -34,7 +34,7 @@ namespace osu.Game.Tests.Beatmaps
                 {
                     Artist = "artist",
                     Title = "title",
-                    Author = new APIUser { Username = "creator" }
+                    Author = new RealmUser { Username = "creator" }
                 }
             };
 
@@ -50,7 +50,7 @@ namespace osu.Game.Tests.Beatmaps
                 {
                     Artist = "artist",
                     Title = "title",
-                    Author = new APIUser { Username = "creator" }
+                    Author = new RealmUser { Username = "creator" }
                 },
                 DifficultyName = "difficulty"
             };
diff --git a/osu.Game.Tests/Database/BeatmapImporterTests.cs b/osu.Game.Tests/Database/BeatmapImporterTests.cs
index e47e24021f..227314cffd 100644
--- a/osu.Game.Tests/Database/BeatmapImporterTests.cs
+++ b/osu.Game.Tests/Database/BeatmapImporterTests.cs
@@ -19,6 +19,7 @@ using osu.Game.Extensions;
 using osu.Game.IO.Archives;
 using osu.Game.Models;
 using osu.Game.Overlays.Notifications;
+using osu.Game.Rulesets;
 using osu.Game.Stores;
 using osu.Game.Tests.Resources;
 using Realms;
@@ -34,33 +35,134 @@ namespace osu.Game.Tests.Database
     [TestFixture]
     public class BeatmapImporterTests : RealmTest
     {
+        [Test]
+        public void TestDetachBeatmapSet()
+        {
+            RunTestWithRealmAsync(async (realmFactory, storage) =>
+            {
+                using (var importer = new BeatmapModelManager(realmFactory, storage))
+                using (new RulesetStore(realmFactory, storage))
+                {
+                    ILive<BeatmapSetInfo>? beatmapSet;
+
+                    using (var reader = new ZipArchiveReader(TestResources.GetTestBeatmapStream()))
+                        beatmapSet = await importer.Import(reader);
+
+                    Assert.NotNull(beatmapSet);
+                    Debug.Assert(beatmapSet != null);
+
+                    BeatmapSetInfo? detachedBeatmapSet = null;
+
+                    beatmapSet.PerformRead(live =>
+                    {
+                        detachedBeatmapSet = live.Detach();
+
+                        // files are omitted
+                        Assert.AreEqual(0, detachedBeatmapSet.Files.Count);
+
+                        Assert.AreEqual(live.Beatmaps.Count, detachedBeatmapSet.Beatmaps.Count);
+                        Assert.AreEqual(live.Beatmaps.Select(f => f.Difficulty).Count(), detachedBeatmapSet.Beatmaps.Select(f => f.Difficulty).Count());
+                        Assert.AreEqual(live.Metadata, detachedBeatmapSet.Metadata);
+                    });
+
+                    Debug.Assert(detachedBeatmapSet != null);
+
+                    // Check detached instances can all be accessed without throwing.
+                    Assert.AreEqual(0, detachedBeatmapSet.Files.Count);
+                    Assert.NotNull(detachedBeatmapSet.Beatmaps.Count);
+                    Assert.NotZero(detachedBeatmapSet.Beatmaps.Select(f => f.Difficulty).Count());
+                    Assert.NotNull(detachedBeatmapSet.Metadata);
+
+                    // Check cyclic reference to beatmap set
+                    Assert.AreEqual(detachedBeatmapSet, detachedBeatmapSet.Beatmaps.First().BeatmapSet);
+                }
+            });
+        }
+
+        [Test]
+        public void TestUpdateDetachedBeatmapSet()
+        {
+            RunTestWithRealmAsync(async (realmFactory, storage) =>
+            {
+                using (var importer = new BeatmapModelManager(realmFactory, storage))
+                using (new RulesetStore(realmFactory, storage))
+                {
+                    ILive<BeatmapSetInfo>? beatmapSet;
+
+                    using (var reader = new ZipArchiveReader(TestResources.GetTestBeatmapStream()))
+                        beatmapSet = await importer.Import(reader);
+
+                    Assert.NotNull(beatmapSet);
+                    Debug.Assert(beatmapSet != null);
+
+                    // Detach at the BeatmapInfo point, similar to what GetWorkingBeatmap does.
+                    BeatmapInfo? detachedBeatmap = null;
+
+                    beatmapSet.PerformRead(s => detachedBeatmap = s.Beatmaps.First().Detach());
+
+                    BeatmapSetInfo? detachedBeatmapSet = detachedBeatmap?.BeatmapSet;
+
+                    Debug.Assert(detachedBeatmapSet != null);
+
+                    var newUser = new RealmUser { Username = "peppy", OnlineID = 2 };
+
+                    detachedBeatmapSet.Beatmaps.First().Metadata.Artist = "New Artist";
+                    detachedBeatmapSet.Beatmaps.First().Metadata.Author = newUser;
+
+                    Assert.AreNotEqual(detachedBeatmapSet.Status, BeatmapOnlineStatus.Ranked);
+                    detachedBeatmapSet.Status = BeatmapOnlineStatus.Ranked;
+
+                    beatmapSet.PerformWrite(s =>
+                    {
+                        detachedBeatmapSet.CopyChangesToRealm(s);
+                    });
+
+                    beatmapSet.PerformRead(s =>
+                    {
+                        // Check above changes explicitly.
+                        Assert.AreEqual(BeatmapOnlineStatus.Ranked, s.Status);
+                        Assert.AreEqual("New Artist", s.Beatmaps.First().Metadata.Artist);
+                        Assert.AreEqual(newUser, s.Beatmaps.First().Metadata.Author);
+                        Assert.NotZero(s.Files.Count);
+
+                        // Check nothing was lost in the copy operation.
+                        Assert.AreEqual(s.Files.Count, detachedBeatmapSet.Files.Count);
+                        Assert.AreEqual(s.Files.Select(f => f.File).Count(), detachedBeatmapSet.Files.Select(f => f.File).Count());
+                        Assert.AreEqual(s.Beatmaps.Count, detachedBeatmapSet.Beatmaps.Count);
+                        Assert.AreEqual(s.Beatmaps.Select(f => f.Difficulty).Count(), detachedBeatmapSet.Beatmaps.Select(f => f.Difficulty).Count());
+                        Assert.AreEqual(s.Metadata, detachedBeatmapSet.Metadata);
+                    });
+                }
+            });
+        }
+
         [Test]
         public void TestImportBeatmapThenCleanup()
         {
             RunTestWithRealmAsync(async (realmFactory, storage) =>
             {
-                using (var importer = new BeatmapImporter(realmFactory, storage))
-                using (new RealmRulesetStore(realmFactory, storage))
+                using (var importer = new BeatmapModelManager(realmFactory, storage))
+                using (new RulesetStore(realmFactory, storage))
                 {
-                    ILive<RealmBeatmapSet>? imported;
+                    ILive<BeatmapSetInfo>? imported;
 
                     using (var reader = new ZipArchiveReader(TestResources.GetTestBeatmapStream()))
                         imported = await importer.Import(reader);
 
-                    Assert.AreEqual(1, realmFactory.Context.All<RealmBeatmapSet>().Count());
+                    Assert.AreEqual(1, realmFactory.Context.All<BeatmapSetInfo>().Count());
 
                     Assert.NotNull(imported);
                     Debug.Assert(imported != null);
 
                     imported.PerformWrite(s => s.DeletePending = true);
 
-                    Assert.AreEqual(1, realmFactory.Context.All<RealmBeatmapSet>().Count(s => s.DeletePending));
+                    Assert.AreEqual(1, realmFactory.Context.All<BeatmapSetInfo>().Count(s => s.DeletePending));
                 }
             });
 
             Logger.Log("Running with no work to purge pending deletions");
 
-            RunTestWithRealm((realmFactory, _) => { Assert.AreEqual(0, realmFactory.Context.All<RealmBeatmapSet>().Count()); });
+            RunTestWithRealm((realmFactory, _) => { Assert.AreEqual(0, realmFactory.Context.All<BeatmapSetInfo>().Count()); });
         }
 
         [Test]
@@ -68,8 +170,8 @@ namespace osu.Game.Tests.Database
         {
             RunTestWithRealmAsync(async (realmFactory, storage) =>
             {
-                using var importer = new BeatmapImporter(realmFactory, storage);
-                using var store = new RealmRulesetStore(realmFactory, storage);
+                using var importer = new BeatmapModelManager(realmFactory, storage);
+                using var store = new RulesetStore(realmFactory, storage);
 
                 await LoadOszIntoStore(importer, realmFactory.Context);
             });
@@ -80,8 +182,8 @@ namespace osu.Game.Tests.Database
         {
             RunTestWithRealmAsync(async (realmFactory, storage) =>
             {
-                using var importer = new BeatmapImporter(realmFactory, storage);
-                using var store = new RealmRulesetStore(realmFactory, storage);
+                using var importer = new BeatmapModelManager(realmFactory, storage);
+                using var store = new RulesetStore(realmFactory, storage);
 
                 var imported = await LoadOszIntoStore(importer, realmFactory.Context);
 
@@ -98,8 +200,8 @@ namespace osu.Game.Tests.Database
         {
             RunTestWithRealmAsync(async (realmFactory, storage) =>
             {
-                using var importer = new BeatmapImporter(realmFactory, storage);
-                using var store = new RealmRulesetStore(realmFactory, storage);
+                using var importer = new BeatmapModelManager(realmFactory, storage);
+                using var store = new RulesetStore(realmFactory, storage);
 
                 var imported = await LoadOszIntoStore(importer, realmFactory.Context);
 
@@ -112,17 +214,17 @@ namespace osu.Game.Tests.Database
         {
             RunTestWithRealmAsync(async (realmFactory, storage) =>
             {
-                using var importer = new BeatmapImporter(realmFactory, storage);
-                using var store = new RealmRulesetStore(realmFactory, storage);
+                using var importer = new BeatmapModelManager(realmFactory, storage);
+                using var store = new RulesetStore(realmFactory, storage);
 
                 string? tempPath = TestResources.GetTestBeatmapForImport();
 
-                ILive<RealmBeatmapSet>? importedSet;
+                ILive<BeatmapSetInfo>? importedSet;
 
                 using (var stream = File.OpenRead(tempPath))
                 {
                     importedSet = await importer.Import(new ImportTask(stream, Path.GetFileName(tempPath)));
-                    ensureLoaded(realmFactory.Context);
+                    EnsureLoaded(realmFactory.Context);
                 }
 
                 Assert.NotNull(importedSet);
@@ -131,7 +233,7 @@ namespace osu.Game.Tests.Database
                 Assert.IsTrue(File.Exists(tempPath), "Stream source file somehow went missing");
                 File.Delete(tempPath);
 
-                var imported = realmFactory.Context.All<RealmBeatmapSet>().First(beatmapSet => beatmapSet.ID == importedSet.ID);
+                var imported = realmFactory.Context.All<BeatmapSetInfo>().First(beatmapSet => beatmapSet.ID == importedSet.ID);
 
                 deleteBeatmapSet(imported, realmFactory.Context);
             });
@@ -142,8 +244,8 @@ namespace osu.Game.Tests.Database
         {
             RunTestWithRealmAsync(async (realmFactory, storage) =>
             {
-                using var importer = new BeatmapImporter(realmFactory, storage);
-                using var store = new RealmRulesetStore(realmFactory, storage);
+                using var importer = new BeatmapModelManager(realmFactory, storage);
+                using var store = new RulesetStore(realmFactory, storage);
 
                 var imported = await LoadOszIntoStore(importer, realmFactory.Context);
                 var importedSecondTime = await LoadOszIntoStore(importer, realmFactory.Context);
@@ -162,8 +264,8 @@ namespace osu.Game.Tests.Database
         {
             RunTestWithRealmAsync(async (realmFactory, storage) =>
             {
-                using var importer = new BeatmapImporter(realmFactory, storage);
-                using var store = new RealmRulesetStore(realmFactory, storage);
+                using var importer = new BeatmapModelManager(realmFactory, storage);
+                using var store = new RulesetStore(realmFactory, storage);
 
                 string? temp = TestResources.GetTestBeatmapForImport();
 
@@ -190,7 +292,7 @@ namespace osu.Game.Tests.Database
 
                     var importedSecondTime = await importer.Import(new ImportTask(temp));
 
-                    ensureLoaded(realmFactory.Context);
+                    EnsureLoaded(realmFactory.Context);
 
                     Assert.NotNull(importedSecondTime);
                     Debug.Assert(importedSecondTime != null);
@@ -211,8 +313,8 @@ namespace osu.Game.Tests.Database
         {
             RunTestWithRealmAsync(async (realmFactory, storage) =>
             {
-                using var importer = new BeatmapImporter(realmFactory, storage);
-                using var store = new RealmRulesetStore(realmFactory, storage);
+                using var importer = new BeatmapModelManager(realmFactory, storage);
+                using var store = new RulesetStore(realmFactory, storage);
 
                 string? temp = TestResources.GetTestBeatmapForImport();
 
@@ -241,7 +343,7 @@ namespace osu.Game.Tests.Database
 
                     var importedSecondTime = await importer.Import(new ImportTask(temp));
 
-                    ensureLoaded(realmFactory.Context);
+                    EnsureLoaded(realmFactory.Context);
 
                     // check the newly "imported" beatmap is not the original.
                     Assert.NotNull(importedSecondTime);
@@ -263,8 +365,8 @@ namespace osu.Game.Tests.Database
         {
             RunTestWithRealmAsync(async (realmFactory, storage) =>
             {
-                using var importer = new BeatmapImporter(realmFactory, storage);
-                using var store = new RealmRulesetStore(realmFactory, storage);
+                using var importer = new BeatmapModelManager(realmFactory, storage);
+                using var store = new RulesetStore(realmFactory, storage);
 
                 string? temp = TestResources.GetTestBeatmapForImport();
 
@@ -290,7 +392,7 @@ namespace osu.Game.Tests.Database
 
                     var importedSecondTime = await importer.Import(new ImportTask(temp));
 
-                    ensureLoaded(realmFactory.Context);
+                    EnsureLoaded(realmFactory.Context);
 
                     Assert.NotNull(importedSecondTime);
                     Debug.Assert(importedSecondTime != null);
@@ -311,8 +413,8 @@ namespace osu.Game.Tests.Database
         {
             RunTestWithRealmAsync(async (realmFactory, storage) =>
             {
-                using var importer = new BeatmapImporter(realmFactory, storage);
-                using var store = new RealmRulesetStore(realmFactory, storage);
+                using var importer = new BeatmapModelManager(realmFactory, storage);
+                using var store = new RulesetStore(realmFactory, storage);
 
                 string? temp = TestResources.GetTestBeatmapForImport();
 
@@ -338,7 +440,7 @@ namespace osu.Game.Tests.Database
 
                     var importedSecondTime = await importer.Import(new ImportTask(temp));
 
-                    ensureLoaded(realmFactory.Context);
+                    EnsureLoaded(realmFactory.Context);
 
                     Assert.NotNull(importedSecondTime);
                     Debug.Assert(importedSecondTime != null);
@@ -360,8 +462,8 @@ namespace osu.Game.Tests.Database
         {
             RunTestWithRealmAsync(async (realmFactory, storage) =>
             {
-                using var importer = new BeatmapImporter(realmFactory, storage);
-                using var store = new RealmRulesetStore(realmFactory, storage);
+                using var importer = new BeatmapModelManager(realmFactory, storage);
+                using var store = new RulesetStore(realmFactory, storage);
 
                 var imported = await LoadOszIntoStore(importer, realmFactory.Context);
 
@@ -393,8 +495,8 @@ namespace osu.Game.Tests.Database
         {
             RunTestWithRealmAsync(async (realmFactory, storage) =>
             {
-                using var importer = new BeatmapImporter(realmFactory, storage);
-                using var store = new RealmRulesetStore(realmFactory, storage);
+                using var importer = new BeatmapModelManager(realmFactory, storage);
+                using var store = new RulesetStore(realmFactory, storage);
 
                 var progressNotification = new ImportProgressNotification();
 
@@ -429,8 +531,8 @@ namespace osu.Game.Tests.Database
                         Interlocked.Increment(ref loggedExceptionCount);
                 };
 
-                using var importer = new BeatmapImporter(realmFactory, storage);
-                using var store = new RealmRulesetStore(realmFactory, storage);
+                using var importer = new BeatmapModelManager(realmFactory, storage);
+                using var store = new RulesetStore(realmFactory, storage);
 
                 var imported = await LoadOszIntoStore(importer, realmFactory.Context);
 
@@ -479,8 +581,8 @@ namespace osu.Game.Tests.Database
         {
             RunTestWithRealmAsync(async (realmFactory, storage) =>
             {
-                using var importer = new BeatmapImporter(realmFactory, storage);
-                using var store = new RealmRulesetStore(realmFactory, storage);
+                using var importer = new BeatmapModelManager(realmFactory, storage);
+                using var store = new RulesetStore(realmFactory, storage);
 
                 var imported = await LoadOszIntoStore(importer, realmFactory.Context);
 
@@ -504,7 +606,7 @@ namespace osu.Game.Tests.Database
             RunTestWithRealmAsync(async (realmFactory, storage) =>
             {
                 using var importer = new NonOptimisedBeatmapImporter(realmFactory, storage);
-                using var store = new RealmRulesetStore(realmFactory, storage);
+                using var store = new RulesetStore(realmFactory, storage);
 
                 var imported = await LoadOszIntoStore(importer, realmFactory.Context);
 
@@ -527,8 +629,8 @@ namespace osu.Game.Tests.Database
         {
             RunTestWithRealmAsync(async (realmFactory, storage) =>
             {
-                using var importer = new BeatmapImporter(realmFactory, storage);
-                using var store = new RealmRulesetStore(realmFactory, storage);
+                using var importer = new BeatmapModelManager(realmFactory, storage);
+                using var store = new RulesetStore(realmFactory, storage);
 
                 var imported = await LoadOszIntoStore(importer, realmFactory.Context);
 
@@ -553,10 +655,10 @@ namespace osu.Game.Tests.Database
         {
             RunTestWithRealmAsync(async (realmFactory, storage) =>
             {
-                using var importer = new BeatmapImporter(realmFactory, storage);
-                using var store = new RealmRulesetStore(realmFactory, storage);
+                using var importer = new BeatmapModelManager(realmFactory, storage);
+                using var store = new RulesetStore(realmFactory, storage);
 
-                var metadata = new RealmBeatmapMetadata
+                var metadata = new BeatmapMetadata
                 {
                     Artist = "SomeArtist",
                     Author =
@@ -565,18 +667,18 @@ namespace osu.Game.Tests.Database
                     }
                 };
 
-                var ruleset = realmFactory.Context.All<RealmRuleset>().First();
+                var ruleset = realmFactory.Context.All<RulesetInfo>().First();
 
-                var toImport = new RealmBeatmapSet
+                var toImport = new BeatmapSetInfo
                 {
                     OnlineID = 1,
                     Beatmaps =
                     {
-                        new RealmBeatmap(ruleset, new RealmBeatmapDifficulty(), metadata)
+                        new BeatmapInfo(ruleset, new BeatmapDifficulty(), metadata)
                         {
                             OnlineID = 2,
                         },
-                        new RealmBeatmap(ruleset, new RealmBeatmapDifficulty(), metadata)
+                        new BeatmapInfo(ruleset, new BeatmapDifficulty(), metadata)
                         {
                             OnlineID = 2,
                             Status = BeatmapOnlineStatus.Loved,
@@ -599,13 +701,13 @@ namespace osu.Game.Tests.Database
         {
             RunTestWithRealmAsync(async (realmFactory, storage) =>
             {
-                using var importer = new BeatmapImporter(realmFactory, storage);
-                using var store = new RealmRulesetStore(realmFactory, storage);
+                using var importer = new BeatmapModelManager(realmFactory, storage);
+                using var store = new RulesetStore(realmFactory, storage);
 
                 string? temp = TestResources.GetTestBeatmapForImport();
                 using (File.OpenRead(temp))
                     await importer.Import(temp);
-                ensureLoaded(realmFactory.Context);
+                EnsureLoaded(realmFactory.Context);
                 File.Delete(temp);
                 Assert.IsFalse(File.Exists(temp), "We likely held a read lock on the file when we shouldn't");
             });
@@ -616,8 +718,8 @@ namespace osu.Game.Tests.Database
         {
             RunTestWithRealmAsync(async (realmFactory, storage) =>
             {
-                using var importer = new BeatmapImporter(realmFactory, storage);
-                using var store = new RealmRulesetStore(realmFactory, storage);
+                using var importer = new BeatmapModelManager(realmFactory, storage);
+                using var store = new RulesetStore(realmFactory, storage);
 
                 string? temp = TestResources.GetTestBeatmapForImport();
 
@@ -638,7 +740,7 @@ namespace osu.Game.Tests.Database
 
                     await importer.Import(temp);
 
-                    ensureLoaded(realmFactory.Context);
+                    EnsureLoaded(realmFactory.Context);
                 }
                 finally
                 {
@@ -652,8 +754,8 @@ namespace osu.Game.Tests.Database
         {
             RunTestWithRealmAsync(async (realmFactory, storage) =>
             {
-                using var importer = new BeatmapImporter(realmFactory, storage);
-                using var store = new RealmRulesetStore(realmFactory, storage);
+                using var importer = new BeatmapModelManager(realmFactory, storage);
+                using var store = new RulesetStore(realmFactory, storage);
 
                 string? temp = TestResources.GetTestBeatmapForImport();
 
@@ -678,7 +780,7 @@ namespace osu.Game.Tests.Database
                     Assert.NotNull(imported);
                     Debug.Assert(imported != null);
 
-                    ensureLoaded(realmFactory.Context);
+                    EnsureLoaded(realmFactory.Context);
 
                     Assert.IsFalse(imported.PerformRead(s => s.Files.Any(f => f.Filename.Contains("subfolder"))), "Files contain common subfolder");
                 }
@@ -694,8 +796,8 @@ namespace osu.Game.Tests.Database
         {
             RunTestWithRealmAsync(async (realmFactory, storage) =>
             {
-                using var importer = new BeatmapImporter(realmFactory, storage);
-                using var store = new RealmRulesetStore(realmFactory, storage);
+                using var importer = new BeatmapModelManager(realmFactory, storage);
+                using var store = new RulesetStore(realmFactory, storage);
 
                 string? temp = TestResources.GetTestBeatmapForImport();
 
@@ -728,7 +830,7 @@ namespace osu.Game.Tests.Database
                     Assert.NotNull(imported);
                     Debug.Assert(imported != null);
 
-                    ensureLoaded(realmFactory.Context);
+                    EnsureLoaded(realmFactory.Context);
 
                     Assert.IsFalse(imported.PerformRead(s => s.Files.Any(f => f.Filename.Contains("__MACOSX"))), "Files contain resource fork folder, which should be ignored");
                     Assert.IsFalse(imported.PerformRead(s => s.Files.Any(f => f.Filename.Contains("actual_data"))), "Files contain common subfolder");
@@ -745,25 +847,25 @@ namespace osu.Game.Tests.Database
         {
             RunTestWithRealmAsync(async (realmFactory, storage) =>
             {
-                using var importer = new BeatmapImporter(realmFactory, storage);
-                using var store = new RealmRulesetStore(realmFactory, storage);
+                using var importer = new BeatmapModelManager(realmFactory, storage);
+                using var store = new RulesetStore(realmFactory, storage);
 
                 string? temp = TestResources.GetTestBeatmapForImport();
                 await importer.Import(temp);
 
                 // Update via the beatmap, not the beatmap info, to ensure correct linking
-                RealmBeatmapSet setToUpdate = realmFactory.Context.All<RealmBeatmapSet>().First();
+                BeatmapSetInfo setToUpdate = realmFactory.Context.All<BeatmapSetInfo>().First();
 
                 var beatmapToUpdate = setToUpdate.Beatmaps.First();
 
                 realmFactory.Context.Write(() => beatmapToUpdate.DifficultyName = "updated");
 
-                RealmBeatmap updatedInfo = realmFactory.Context.All<RealmBeatmap>().First(b => b.ID == beatmapToUpdate.ID);
+                BeatmapInfo updatedInfo = realmFactory.Context.All<BeatmapInfo>().First(b => b.ID == beatmapToUpdate.ID);
                 Assert.That(updatedInfo.DifficultyName, Is.EqualTo("updated"));
             });
         }
 
-        public static async Task<RealmBeatmapSet?> LoadQuickOszIntoOsu(BeatmapImporter importer, Realm realm)
+        public static async Task<BeatmapSetInfo?> LoadQuickOszIntoOsu(BeatmapImporter importer, Realm realm)
         {
             string? temp = TestResources.GetQuickTestBeatmapForImport();
 
@@ -771,14 +873,14 @@ namespace osu.Game.Tests.Database
 
             Assert.NotNull(importedSet);
 
-            ensureLoaded(realm);
+            EnsureLoaded(realm);
 
             waitForOrAssert(() => !File.Exists(temp), "Temporary file still exists after standard import", 5000);
 
-            return realm.All<RealmBeatmapSet>().FirstOrDefault(beatmapSet => beatmapSet.ID == importedSet!.ID);
+            return realm.All<BeatmapSetInfo>().FirstOrDefault(beatmapSet => beatmapSet.ID == importedSet!.ID);
         }
 
-        public static async Task<RealmBeatmapSet> LoadOszIntoStore(BeatmapImporter importer, Realm realm, string? path = null, bool virtualTrack = false)
+        public static async Task<BeatmapSetInfo> LoadOszIntoStore(BeatmapImporter importer, Realm realm, string? path = null, bool virtualTrack = false)
         {
             string? temp = path ?? TestResources.GetTestBeatmapForImport(virtualTrack);
 
@@ -787,24 +889,24 @@ namespace osu.Game.Tests.Database
             Assert.NotNull(importedSet);
             Debug.Assert(importedSet != null);
 
-            ensureLoaded(realm);
+            EnsureLoaded(realm);
 
             waitForOrAssert(() => !File.Exists(temp), "Temporary file still exists after standard import", 5000);
 
-            return realm.All<RealmBeatmapSet>().First(beatmapSet => beatmapSet.ID == importedSet.ID);
+            return realm.All<BeatmapSetInfo>().First(beatmapSet => beatmapSet.ID == importedSet.ID);
         }
 
-        private void deleteBeatmapSet(RealmBeatmapSet imported, Realm realm)
+        private void deleteBeatmapSet(BeatmapSetInfo imported, Realm realm)
         {
             realm.Write(() => imported.DeletePending = true);
 
             checkBeatmapSetCount(realm, 0);
             checkBeatmapSetCount(realm, 1, true);
 
-            Assert.IsTrue(realm.All<RealmBeatmapSet>().First(_ => true).DeletePending);
+            Assert.IsTrue(realm.All<BeatmapSetInfo>().First(_ => true).DeletePending);
         }
 
-        private static Task createScoreForBeatmap(Realm realm, RealmBeatmap beatmap)
+        private static Task createScoreForBeatmap(Realm realm, BeatmapInfo beatmap)
         {
             // TODO: reimplement when we have score support in realm.
             // return ImportScoreTest.LoadScoreIntoOsu(osu, new ScoreInfo
@@ -820,8 +922,8 @@ namespace osu.Game.Tests.Database
         private static void checkBeatmapSetCount(Realm realm, int expected, bool includeDeletePending = false)
         {
             Assert.AreEqual(expected, includeDeletePending
-                ? realm.All<RealmBeatmapSet>().Count()
-                : realm.All<RealmBeatmapSet>().Count(s => !s.DeletePending));
+                ? realm.All<BeatmapSetInfo>().Count()
+                : realm.All<BeatmapSetInfo>().Count(s => !s.DeletePending));
         }
 
         private static string hashFile(string filename)
@@ -832,7 +934,7 @@ namespace osu.Game.Tests.Database
 
         private static void checkBeatmapCount(Realm realm, int expected)
         {
-            Assert.AreEqual(expected, realm.All<RealmBeatmap>().Where(_ => true).ToList().Count);
+            Assert.AreEqual(expected, realm.All<BeatmapInfo>().Where(_ => true).ToList().Count);
         }
 
         private static void checkSingleReferencedFileCount(Realm realm, int expected)
@@ -848,26 +950,25 @@ namespace osu.Game.Tests.Database
             Assert.AreEqual(expected, singleReferencedCount);
         }
 
-        private static void ensureLoaded(Realm realm, int timeout = 60000)
+        internal static void EnsureLoaded(Realm realm, int timeout = 60000)
         {
-            IQueryable<RealmBeatmapSet>? resultSets = null;
+            IQueryable<BeatmapSetInfo>? resultSets = null;
 
             waitForOrAssert(() =>
-                {
-                    realm.Refresh();
-                    return (resultSets = realm.All<RealmBeatmapSet>().Where(s => !s.DeletePending && s.OnlineID == 241526)).Any();
-                },
-                @"BeatmapSet did not import to the database in allocated time.", timeout);
+            {
+                realm.Refresh();
+                return (resultSets = realm.All<BeatmapSetInfo>().Where(s => !s.DeletePending && s.OnlineID == 241526)).Any();
+            }, @"BeatmapSet did not import to the database in allocated time.", timeout);
 
             // ensure we were stored to beatmap database backing...
             Assert.IsTrue(resultSets?.Count() == 1, $@"Incorrect result count found ({resultSets?.Count()} but should be 1).");
 
-            IEnumerable<RealmBeatmapSet> queryBeatmapSets() => realm.All<RealmBeatmapSet>().Where(s => !s.DeletePending && s.OnlineID == 241526);
+            IEnumerable<BeatmapSetInfo> queryBeatmapSets() => realm.All<BeatmapSetInfo>().Where(s => !s.DeletePending && s.OnlineID == 241526);
 
             var set = queryBeatmapSets().First();
 
             // ReSharper disable once PossibleUnintendedReferenceComparison
-            IEnumerable<RealmBeatmap> queryBeatmaps() => realm.All<RealmBeatmap>().Where(s => s.BeatmapSet != null && s.BeatmapSet == set);
+            IEnumerable<BeatmapInfo> queryBeatmaps() => realm.All<BeatmapInfo>().Where(s => s.BeatmapSet != null && s.BeatmapSet == set);
 
             Assert.AreEqual(12, queryBeatmaps().Count(), @"Beatmap count was not correct");
             Assert.AreEqual(1, queryBeatmapSets().Count(), @"Beatmapset count was not correct");
@@ -880,7 +981,7 @@ namespace osu.Game.Tests.Database
                 countBeatmaps = queryBeatmaps().Count(),
                 $@"Incorrect database beatmap count post-import ({countBeatmaps} but should be {countBeatmapSetBeatmaps}).");
 
-            foreach (RealmBeatmap b in set.Beatmaps)
+            foreach (BeatmapInfo b in set.Beatmaps)
                 Assert.IsTrue(set.Beatmaps.Any(c => c.OnlineID == b.OnlineID));
             Assert.IsTrue(set.Beatmaps.Count > 0);
         }
diff --git a/osu.Game.Tests/Database/GeneralUsageTests.cs b/osu.Game.Tests/Database/GeneralUsageTests.cs
index 2285b22a3a..0961ad71e4 100644
--- a/osu.Game.Tests/Database/GeneralUsageTests.cs
+++ b/osu.Game.Tests/Database/GeneralUsageTests.cs
@@ -5,8 +5,8 @@ using System;
 using System.Threading;
 using System.Threading.Tasks;
 using NUnit.Framework;
+using osu.Game.Beatmaps;
 using osu.Game.Database;
-using osu.Game.Models;
 
 #nullable enable
 
@@ -48,7 +48,7 @@ namespace osu.Game.Tests.Database
 
                 using (var context = realmFactory.CreateContext())
                 {
-                    var subscription = context.All<RealmBeatmap>().QueryAsyncWithNotifications((sender, changes, error) =>
+                    var subscription = context.All<BeatmapInfo>().QueryAsyncWithNotifications((sender, changes, error) =>
                     {
                         using (realmFactory.CreateContext())
                         {
diff --git a/osu.Game.Tests/Database/RealmLiveTests.cs b/osu.Game.Tests/Database/RealmLiveTests.cs
index 9432a56741..187fcd3ca7 100644
--- a/osu.Game.Tests/Database/RealmLiveTests.cs
+++ b/osu.Game.Tests/Database/RealmLiveTests.cs
@@ -8,8 +8,8 @@ using System.Threading.Tasks;
 using NUnit.Framework;
 using osu.Framework.Extensions;
 using osu.Framework.Testing;
+using osu.Game.Beatmaps;
 using osu.Game.Database;
-using osu.Game.Models;
 using Realms;
 
 #nullable enable
@@ -23,9 +23,9 @@ namespace osu.Game.Tests.Database
         {
             RunTestWithRealm((realmFactory, _) =>
             {
-                ILive<RealmBeatmap> beatmap = realmFactory.CreateContext().Write(r => r.Add(new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata()))).ToLive(realmFactory);
+                ILive<BeatmapInfo> beatmap = realmFactory.CreateContext().Write(r => r.Add(new BeatmapInfo(CreateRuleset(), new BeatmapDifficulty(), new BeatmapMetadata()))).ToLive(realmFactory);
 
-                ILive<RealmBeatmap> beatmap2 = realmFactory.CreateContext().All<RealmBeatmap>().First().ToLive(realmFactory);
+                ILive<BeatmapInfo> beatmap2 = realmFactory.CreateContext().All<BeatmapInfo>().First().ToLive(realmFactory);
 
                 Assert.AreEqual(beatmap, beatmap2);
             });
@@ -36,9 +36,9 @@ namespace osu.Game.Tests.Database
         {
             RunTestWithRealm((realmFactory, storage) =>
             {
-                var beatmap = new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata());
+                var beatmap = new BeatmapInfo(CreateRuleset(), new BeatmapDifficulty(), new BeatmapMetadata());
 
-                ILive<RealmBeatmap> liveBeatmap;
+                ILive<BeatmapInfo> liveBeatmap;
 
                 using (var context = realmFactory.CreateContext())
                 {
@@ -63,7 +63,7 @@ namespace osu.Game.Tests.Database
         {
             RunTestWithRealm((realmFactory, _) =>
             {
-                var beatmap = new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata());
+                var beatmap = new BeatmapInfo(CreateRuleset(), new BeatmapDifficulty(), new BeatmapMetadata());
 
                 var liveBeatmap = beatmap.ToLive(realmFactory);
 
@@ -77,7 +77,7 @@ namespace osu.Game.Tests.Database
         [Test]
         public void TestAccessNonManaged()
         {
-            var beatmap = new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata());
+            var beatmap = new BeatmapInfo(CreateRuleset(), new BeatmapDifficulty(), new BeatmapMetadata());
             var liveBeatmap = beatmap.ToLiveUnmanaged();
 
             Assert.IsFalse(beatmap.Hidden);
@@ -96,12 +96,12 @@ namespace osu.Game.Tests.Database
         {
             RunTestWithRealm((realmFactory, _) =>
             {
-                ILive<RealmBeatmap>? liveBeatmap = null;
+                ILive<BeatmapInfo>? liveBeatmap = null;
                 Task.Factory.StartNew(() =>
                 {
                     using (var threadContext = realmFactory.CreateContext())
                     {
-                        var beatmap = threadContext.Write(r => r.Add(new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata())));
+                        var beatmap = threadContext.Write(r => r.Add(new BeatmapInfo(CreateRuleset(), new BeatmapDifficulty(), new BeatmapMetadata())));
 
                         liveBeatmap = beatmap.ToLive(realmFactory);
                     }
@@ -125,12 +125,12 @@ namespace osu.Game.Tests.Database
         {
             RunTestWithRealm((realmFactory, _) =>
             {
-                ILive<RealmBeatmap>? liveBeatmap = null;
+                ILive<BeatmapInfo>? liveBeatmap = null;
                 Task.Factory.StartNew(() =>
                 {
                     using (var threadContext = realmFactory.CreateContext())
                     {
-                        var beatmap = threadContext.Write(r => r.Add(new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata())));
+                        var beatmap = threadContext.Write(r => r.Add(new BeatmapInfo(CreateRuleset(), new BeatmapDifficulty(), new BeatmapMetadata())));
 
                         liveBeatmap = beatmap.ToLive(realmFactory);
                     }
@@ -151,7 +151,7 @@ namespace osu.Game.Tests.Database
         {
             RunTestWithRealm((realmFactory, _) =>
             {
-                var beatmap = new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata());
+                var beatmap = new BeatmapInfo(CreateRuleset(), new BeatmapDifficulty(), new BeatmapMetadata());
                 var liveBeatmap = beatmap.ToLive(realmFactory);
 
                 Assert.DoesNotThrow(() =>
@@ -166,13 +166,13 @@ namespace osu.Game.Tests.Database
         {
             RunTestWithRealm((realmFactory, _) =>
             {
-                ILive<RealmBeatmap>? liveBeatmap = null;
+                ILive<BeatmapInfo>? liveBeatmap = null;
 
                 Task.Factory.StartNew(() =>
                 {
                     using (var threadContext = realmFactory.CreateContext())
                     {
-                        var beatmap = threadContext.Write(r => r.Add(new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata())));
+                        var beatmap = threadContext.Write(r => r.Add(new BeatmapInfo(CreateRuleset(), new BeatmapDifficulty(), new BeatmapMetadata())));
 
                         liveBeatmap = beatmap.ToLive(realmFactory);
                     }
@@ -205,12 +205,12 @@ namespace osu.Game.Tests.Database
         {
             RunTestWithRealm((realmFactory, _) =>
             {
-                ILive<RealmBeatmap>? liveBeatmap = null;
+                ILive<BeatmapInfo>? liveBeatmap = null;
                 Task.Factory.StartNew(() =>
                 {
                     using (var threadContext = realmFactory.CreateContext())
                     {
-                        var beatmap = threadContext.Write(r => r.Add(new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata())));
+                        var beatmap = threadContext.Write(r => r.Add(new BeatmapInfo(CreateRuleset(), new BeatmapDifficulty(), new BeatmapMetadata())));
 
                         liveBeatmap = beatmap.ToLive(realmFactory);
                     }
@@ -237,19 +237,19 @@ namespace osu.Game.Tests.Database
 
                 using (var updateThreadContext = realmFactory.CreateContext())
                 {
-                    updateThreadContext.All<RealmBeatmap>().QueryAsyncWithNotifications(gotChange);
-                    ILive<RealmBeatmap>? liveBeatmap = null;
+                    updateThreadContext.All<BeatmapInfo>().QueryAsyncWithNotifications(gotChange);
+                    ILive<BeatmapInfo>? liveBeatmap = null;
 
                     Task.Factory.StartNew(() =>
                     {
                         using (var threadContext = realmFactory.CreateContext())
                         {
                             var ruleset = CreateRuleset();
-                            var beatmap = threadContext.Write(r => r.Add(new RealmBeatmap(ruleset, new RealmBeatmapDifficulty(), new RealmBeatmapMetadata())));
+                            var beatmap = threadContext.Write(r => r.Add(new BeatmapInfo(ruleset, new BeatmapDifficulty(), new BeatmapMetadata())));
 
                             // add a second beatmap to ensure that a full refresh occurs below.
                             // not just a refresh from the resolved Live.
-                            threadContext.Write(r => r.Add(new RealmBeatmap(ruleset, new RealmBeatmapDifficulty(), new RealmBeatmapMetadata())));
+                            threadContext.Write(r => r.Add(new BeatmapInfo(ruleset, new BeatmapDifficulty(), new BeatmapMetadata())));
 
                             liveBeatmap = beatmap.ToLive(realmFactory);
                         }
@@ -258,14 +258,14 @@ namespace osu.Game.Tests.Database
                     Debug.Assert(liveBeatmap != null);
 
                     // not yet seen by main context
-                    Assert.AreEqual(0, updateThreadContext.All<RealmBeatmap>().Count());
+                    Assert.AreEqual(0, updateThreadContext.All<BeatmapInfo>().Count());
                     Assert.AreEqual(0, changesTriggered);
 
                     liveBeatmap.PerformRead(resolved =>
                     {
                         // retrieval causes an implicit refresh. even changes that aren't related to the retrieval are fired at this point.
                         // ReSharper disable once AccessToDisposedClosure
-                        Assert.AreEqual(2, updateThreadContext.All<RealmBeatmap>().Count());
+                        Assert.AreEqual(2, updateThreadContext.All<BeatmapInfo>().Count());
                         Assert.AreEqual(1, changesTriggered);
 
                         // can access properties without a crash.
@@ -280,7 +280,7 @@ namespace osu.Game.Tests.Database
                     });
                 }
 
-                void gotChange(IRealmCollection<RealmBeatmap> sender, ChangeSet changes, Exception error)
+                void gotChange(IRealmCollection<BeatmapInfo> sender, ChangeSet changes, Exception error)
                 {
                     changesTriggered++;
                 }
diff --git a/osu.Game.Tests/Database/RealmTest.cs b/osu.Game.Tests/Database/RealmTest.cs
index 4e67f09dca..0cee165f75 100644
--- a/osu.Game.Tests/Database/RealmTest.cs
+++ b/osu.Game.Tests/Database/RealmTest.cs
@@ -9,9 +9,11 @@ using osu.Framework.Extensions;
 using osu.Framework.Logging;
 using osu.Framework.Platform;
 using osu.Framework.Testing;
+using osu.Game.Beatmaps;
 using osu.Game.Database;
 using osu.Game.IO;
 using osu.Game.Models;
+using osu.Game.Rulesets;
 
 #nullable enable
 
@@ -74,24 +76,24 @@ namespace osu.Game.Tests.Database
             }
         }
 
-        protected static RealmBeatmapSet CreateBeatmapSet(RealmRuleset ruleset)
+        protected static BeatmapSetInfo CreateBeatmapSet(RulesetInfo ruleset)
         {
             RealmFile createRealmFile() => new RealmFile { Hash = Guid.NewGuid().ToString().ComputeSHA2Hash() };
 
-            var metadata = new RealmBeatmapMetadata
+            var metadata = new BeatmapMetadata
             {
                 Title = "My Love",
                 Artist = "Kuba Oms"
             };
 
-            var beatmapSet = new RealmBeatmapSet
+            var beatmapSet = new BeatmapSetInfo
             {
                 Beatmaps =
                 {
-                    new RealmBeatmap(ruleset, new RealmBeatmapDifficulty(), metadata) { DifficultyName = "Easy", },
-                    new RealmBeatmap(ruleset, new RealmBeatmapDifficulty(), metadata) { DifficultyName = "Normal", },
-                    new RealmBeatmap(ruleset, new RealmBeatmapDifficulty(), metadata) { DifficultyName = "Hard", },
-                    new RealmBeatmap(ruleset, new RealmBeatmapDifficulty(), metadata) { DifficultyName = "Insane", }
+                    new BeatmapInfo(ruleset, new BeatmapDifficulty(), metadata) { DifficultyName = "Easy", },
+                    new BeatmapInfo(ruleset, new BeatmapDifficulty(), metadata) { DifficultyName = "Normal", },
+                    new BeatmapInfo(ruleset, new BeatmapDifficulty(), metadata) { DifficultyName = "Hard", },
+                    new BeatmapInfo(ruleset, new BeatmapDifficulty(), metadata) { DifficultyName = "Insane", }
                 },
                 Files =
                 {
@@ -111,8 +113,8 @@ namespace osu.Game.Tests.Database
             return beatmapSet;
         }
 
-        protected static RealmRuleset CreateRuleset() =>
-            new RealmRuleset(0, "osu!", "osu", true);
+        protected static RulesetInfo CreateRuleset() =>
+            new RulesetInfo(0, "osu!", "osu", true);
 
         private class RealmTestGame : Framework.Game
         {
diff --git a/osu.Game.Tests/Database/RulesetStoreTests.cs b/osu.Game.Tests/Database/RulesetStoreTests.cs
index cc7e8a0c97..4416da6f92 100644
--- a/osu.Game.Tests/Database/RulesetStoreTests.cs
+++ b/osu.Game.Tests/Database/RulesetStoreTests.cs
@@ -3,8 +3,7 @@
 
 using System.Linq;
 using NUnit.Framework;
-using osu.Game.Models;
-using osu.Game.Stores;
+using osu.Game.Rulesets;
 
 namespace osu.Game.Tests.Database
 {
@@ -15,10 +14,10 @@ namespace osu.Game.Tests.Database
         {
             RunTestWithRealm((realmFactory, storage) =>
             {
-                var rulesets = new RealmRulesetStore(realmFactory, storage);
+                var rulesets = new RulesetStore(realmFactory, storage);
 
                 Assert.AreEqual(4, rulesets.AvailableRulesets.Count());
-                Assert.AreEqual(4, realmFactory.Context.All<RealmRuleset>().Count());
+                Assert.AreEqual(4, realmFactory.Context.All<RulesetInfo>().Count());
             });
         }
 
@@ -27,14 +26,14 @@ namespace osu.Game.Tests.Database
         {
             RunTestWithRealm((realmFactory, storage) =>
             {
-                var rulesets = new RealmRulesetStore(realmFactory, storage);
-                var rulesets2 = new RealmRulesetStore(realmFactory, storage);
+                var rulesets = new RulesetStore(realmFactory, storage);
+                var rulesets2 = new RulesetStore(realmFactory, storage);
 
                 Assert.AreEqual(4, rulesets.AvailableRulesets.Count());
                 Assert.AreEqual(4, rulesets2.AvailableRulesets.Count());
 
                 Assert.AreEqual(rulesets.AvailableRulesets.First(), rulesets2.AvailableRulesets.First());
-                Assert.AreEqual(4, realmFactory.Context.All<RealmRuleset>().Count());
+                Assert.AreEqual(4, realmFactory.Context.All<RulesetInfo>().Count());
             });
         }
 
@@ -43,7 +42,7 @@ namespace osu.Game.Tests.Database
         {
             RunTestWithRealm((realmFactory, storage) =>
             {
-                var rulesets = new RealmRulesetStore(realmFactory, storage);
+                var rulesets = new RulesetStore(realmFactory, storage);
 
                 Assert.IsFalse(rulesets.AvailableRulesets.First().IsManaged);
                 Assert.IsFalse(rulesets.GetRuleset(0)?.IsManaged);
diff --git a/osu.Game.Tests/Editing/Checks/CheckAudioInVideoTest.cs b/osu.Game.Tests/Editing/Checks/CheckAudioInVideoTest.cs
index f9b7bfa586..614b9b4ac1 100644
--- a/osu.Game.Tests/Editing/Checks/CheckAudioInVideoTest.cs
+++ b/osu.Game.Tests/Editing/Checks/CheckAudioInVideoTest.cs
@@ -74,7 +74,7 @@ namespace osu.Game.Tests.Editing.Checks
         [Test]
         public void TestMissingFile()
         {
-            beatmap.BeatmapInfo.BeatmapSet.Files.Clear();
+            beatmap.BeatmapInfo.BeatmapSet?.Files.Clear();
 
             var issues = check.Run(getContext(null)).ToList();
 
diff --git a/osu.Game.Tests/Editing/Checks/CheckFilePresenceTest.cs b/osu.Game.Tests/Editing/Checks/CheckFilePresenceTest.cs
index f36454aa71..01baaadc7d 100644
--- a/osu.Game.Tests/Editing/Checks/CheckFilePresenceTest.cs
+++ b/osu.Game.Tests/Editing/Checks/CheckFilePresenceTest.cs
@@ -46,7 +46,7 @@ namespace osu.Game.Tests.Editing.Checks
         [Test]
         public void TestBackgroundSetAndNotInFiles()
         {
-            beatmap.BeatmapInfo.BeatmapSet.Files.Clear();
+            beatmap.BeatmapInfo.BeatmapSet?.Files.Clear();
 
             var context = new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap));
             var issues = check.Run(context).ToList();
diff --git a/osu.Game.Tests/Editing/Checks/CheckTestHelpers.cs b/osu.Game.Tests/Editing/Checks/CheckTestHelpers.cs
index f702921986..9067714ff9 100644
--- a/osu.Game.Tests/Editing/Checks/CheckTestHelpers.cs
+++ b/osu.Game.Tests/Editing/Checks/CheckTestHelpers.cs
@@ -1,18 +1,13 @@
 // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
 // See the LICENCE file in the repository root for full licence text.
 
-using osu.Game.Beatmaps;
-using osu.Game.IO;
+using osu.Game.Models;
 
 namespace osu.Game.Tests.Editing.Checks
 {
     public static class CheckTestHelpers
     {
-        public static BeatmapSetFileInfo CreateMockFile(string extension) =>
-            new BeatmapSetFileInfo
-            {
-                Filename = $"abc123.{extension}",
-                FileInfo = new FileInfo { Hash = "abcdef" }
-            };
+        public static RealmNamedFileUsage CreateMockFile(string extension) =>
+            new RealmNamedFileUsage(new RealmFile { Hash = "abcdef" }, $"abc123.{extension}");
     }
 }
diff --git a/osu.Game.Tests/Editing/Checks/CheckTooShortAudioFilesTest.cs b/osu.Game.Tests/Editing/Checks/CheckTooShortAudioFilesTest.cs
index 8adf0d3764..242fec2f68 100644
--- a/osu.Game.Tests/Editing/Checks/CheckTooShortAudioFilesTest.cs
+++ b/osu.Game.Tests/Editing/Checks/CheckTooShortAudioFilesTest.cs
@@ -1,6 +1,7 @@
 // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
 // See the LICENCE file in the repository root for full licence text.
 
+using System.Diagnostics;
 using System.IO;
 using System.Linq;
 using ManagedBass;
@@ -45,6 +46,8 @@ namespace osu.Game.Tests.Editing.Checks
         [Test]
         public void TestDifferentExtension()
         {
+            Debug.Assert(beatmap.BeatmapInfo.BeatmapSet != null);
+
             beatmap.BeatmapInfo.BeatmapSet.Files.Clear();
             beatmap.BeatmapInfo.BeatmapSet.Files.Add(CheckTestHelpers.CreateMockFile("jpg"));
 
diff --git a/osu.Game.Tests/Editing/EditorChangeHandlerTest.cs b/osu.Game.Tests/Editing/EditorChangeHandlerTest.cs
index 481cb3230e..2d61948a2a 100644
--- a/osu.Game.Tests/Editing/EditorChangeHandlerTest.cs
+++ b/osu.Game.Tests/Editing/EditorChangeHandlerTest.cs
@@ -2,7 +2,8 @@
 // See the LICENCE file in the repository root for full licence text.
 
 using NUnit.Framework;
-using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Osu;
+using osu.Game.Rulesets.Osu.Beatmaps;
 using osu.Game.Rulesets.Osu.Objects;
 using osu.Game.Screens.Edit;
 
@@ -158,7 +159,13 @@ namespace osu.Game.Tests.Editing
 
         private (EditorChangeHandler, EditorBeatmap) createChangeHandler()
         {
-            var beatmap = new EditorBeatmap(new Beatmap());
+            var beatmap = new EditorBeatmap(new OsuBeatmap
+            {
+                BeatmapInfo =
+                {
+                    Ruleset = new OsuRuleset().RulesetInfo,
+                },
+            });
 
             var changeHandler = new EditorChangeHandler(beatmap);
 
diff --git a/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs b/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs
index 8eb9452736..43f22e4e90 100644
--- a/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs
+++ b/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs
@@ -35,7 +35,13 @@ namespace osu.Game.Tests.Editing
                 RelativeSizeAxes = Axes.Both,
                 Children = new Drawable[]
                 {
-                    editorBeatmap = new EditorBeatmap(new OsuBeatmap()),
+                    editorBeatmap = new EditorBeatmap(new OsuBeatmap
+                    {
+                        BeatmapInfo =
+                        {
+                            Ruleset = new OsuRuleset().RulesetInfo,
+                        },
+                    }),
                     Content = new Container
                     {
                         RelativeSizeAxes = Axes.Both,
diff --git a/osu.Game.Tests/NonVisual/BeatmapSetInfoEqualityTest.cs b/osu.Game.Tests/NonVisual/BeatmapSetInfoEqualityTest.cs
index 534983f869..1b6049fcb7 100644
--- a/osu.Game.Tests/NonVisual/BeatmapSetInfoEqualityTest.cs
+++ b/osu.Game.Tests/NonVisual/BeatmapSetInfoEqualityTest.cs
@@ -1,6 +1,7 @@
 // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
 // See the LICENCE file in the repository root for full licence text.
 
+using System;
 using NUnit.Framework;
 using osu.Game.Beatmaps;
 using osu.Game.Extensions;
@@ -23,8 +24,10 @@ namespace osu.Game.Tests.NonVisual
         [Test]
         public void TestDatabasedWithDatabased()
         {
-            var ourInfo = new BeatmapSetInfo { ID = 123 };
-            var otherInfo = new BeatmapSetInfo { ID = 123 };
+            var guid = Guid.NewGuid();
+
+            var ourInfo = new BeatmapSetInfo { ID = guid };
+            var otherInfo = new BeatmapSetInfo { ID = guid };
 
             Assert.AreEqual(ourInfo, otherInfo);
         }
@@ -32,7 +35,7 @@ namespace osu.Game.Tests.NonVisual
         [Test]
         public void TestDatabasedWithOnline()
         {
-            var ourInfo = new BeatmapSetInfo { ID = 123, OnlineID = 12 };
+            var ourInfo = new BeatmapSetInfo { ID = Guid.NewGuid(), OnlineID = 12 };
             var otherInfo = new BeatmapSetInfo { OnlineID = 12 };
 
             Assert.AreNotEqual(ourInfo, otherInfo);
diff --git a/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs b/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs
index 4bb54f1625..61ef31e07e 100644
--- a/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs
+++ b/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs
@@ -179,7 +179,7 @@ namespace osu.Game.Tests.NonVisual
                 {
                     var osu = LoadOsuIntoHost(host);
 
-                    const string database_filename = "client.db";
+                    const string database_filename = "client.realm";
 
                     Assert.DoesNotThrow(() => osu.Migrate(customPath));
                     Assert.That(File.Exists(Path.Combine(customPath, database_filename)));
diff --git a/osu.Game.Tests/NonVisual/Filtering/FilterMatchingTest.cs b/osu.Game.Tests/NonVisual/Filtering/FilterMatchingTest.cs
index 8ba3d1a6c7..74904f4585 100644
--- a/osu.Game.Tests/NonVisual/Filtering/FilterMatchingTest.cs
+++ b/osu.Game.Tests/NonVisual/Filtering/FilterMatchingTest.cs
@@ -17,9 +17,8 @@ namespace osu.Game.Tests.NonVisual.Filtering
         private BeatmapInfo getExampleBeatmap() => new BeatmapInfo
         {
             Ruleset = new RulesetInfo { OnlineID = 0 },
-            RulesetID = 0,
             StarRating = 4.0d,
-            BaseDifficulty = new BeatmapDifficulty
+            Difficulty = new BeatmapDifficulty
             {
                 ApproachRate = 5.0f,
                 DrainRate = 3.0f,
@@ -31,7 +30,7 @@ namespace osu.Game.Tests.NonVisual.Filtering
                 ArtistUnicode = "check unicode too",
                 Title = "Title goes here",
                 TitleUnicode = "Title goes here",
-                AuthorString = "The Author",
+                Author = { Username = "The Author" },
                 Source = "unit tests",
                 Tags = "look for tags too",
             },
diff --git a/osu.Game.Tests/Online/TestAPIModJsonSerialization.cs b/osu.Game.Tests/Online/TestAPIModJsonSerialization.cs
index 4b160e1d67..1b7a7656b5 100644
--- a/osu.Game.Tests/Online/TestAPIModJsonSerialization.cs
+++ b/osu.Game.Tests/Online/TestAPIModJsonSerialization.cs
@@ -9,10 +9,12 @@ using osu.Framework.Bindables;
 using osu.Game.Beatmaps;
 using osu.Game.Configuration;
 using osu.Game.Online.API;
+using osu.Game.Online.API.Requests.Responses;
 using osu.Game.Online.Solo;
 using osu.Game.Rulesets;
 using osu.Game.Rulesets.Difficulty;
 using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Osu;
 using osu.Game.Rulesets.Osu.Mods;
 using osu.Game.Rulesets.UI;
 using osu.Game.Scoring;
@@ -93,7 +95,11 @@ namespace osu.Game.Tests.Online
         [Test]
         public void TestDeserialiseSubmittableScoreWithEmptyMods()
         {
-            var score = new SubmittableScore(new ScoreInfo());
+            var score = new SubmittableScore(new ScoreInfo
+            {
+                User = new APIUser(),
+                Ruleset = new OsuRuleset().RulesetInfo,
+            });
 
             var deserialised = JsonConvert.DeserializeObject<SubmittableScore>(JsonConvert.SerializeObject(score));
 
@@ -105,7 +111,9 @@ namespace osu.Game.Tests.Online
         {
             var score = new SubmittableScore(new ScoreInfo
             {
-                Mods = new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 2 } } }
+                Mods = new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 2 } } },
+                User = new APIUser(),
+                Ruleset = new OsuRuleset().RulesetInfo,
             });
 
             var deserialised = JsonConvert.DeserializeObject<SubmittableScore>(JsonConvert.SerializeObject(score));
diff --git a/osu.Game.Tests/Online/TestSceneBeatmapDownloading.cs b/osu.Game.Tests/Online/TestSceneBeatmapDownloading.cs
index 4e77973655..ad9ea79646 100644
--- a/osu.Game.Tests/Online/TestSceneBeatmapDownloading.cs
+++ b/osu.Game.Tests/Online/TestSceneBeatmapDownloading.cs
@@ -5,6 +5,7 @@ using NUnit.Framework;
 using osu.Framework.Allocation;
 using osu.Framework.Testing;
 using osu.Game.Beatmaps;
+using osu.Game.Models;
 using osu.Game.Online.API.Requests.Responses;
 using osu.Game.Overlays.Notifications;
 using osu.Game.Tests.Visual;
@@ -20,13 +21,19 @@ namespace osu.Game.Tests.Online
         private static readonly BeatmapSetInfo test_db_model = new BeatmapSetInfo
         {
             OnlineID = 1,
-            Metadata = new BeatmapMetadata
+            Beatmaps =
             {
-                Artist = "test author",
-                Title = "test title",
-                Author = new APIUser
+                new BeatmapInfo
                 {
-                    Username = "mapper"
+                    Metadata = new BeatmapMetadata
+                    {
+                        Artist = "test author",
+                        Title = "test title",
+                        Author = new RealmUser
+                        {
+                            Username = "mapper"
+                        }
+                    }
                 }
             }
         };
diff --git a/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs b/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs
index a7b431fb6e..8c24b2eef8 100644
--- a/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs
+++ b/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs
@@ -60,9 +60,8 @@ namespace osu.Game.Tests.Online
             testBeatmapInfo = getTestBeatmapInfo(testBeatmapFile);
             testBeatmapSet = testBeatmapInfo.BeatmapSet;
 
-            var existing = beatmaps.QueryBeatmapSet(s => s.OnlineID == testBeatmapSet.OnlineID);
-            if (existing != null)
-                beatmaps.Delete(existing);
+            ContextFactory.Context.Write(r => r.RemoveAll<BeatmapSetInfo>());
+            ContextFactory.Context.Write(r => r.RemoveAll<BeatmapInfo>());
 
             selectedItem.Value = new PlaylistItem
             {
@@ -103,10 +102,10 @@ namespace osu.Game.Tests.Online
             AddStep("import beatmap", () => beatmaps.Import(testBeatmapFile).WaitSafely());
             addAvailabilityCheckStep("state locally available", BeatmapAvailability.LocallyAvailable);
 
-            AddStep("delete beatmap", () => beatmaps.Delete(beatmaps.QueryBeatmapSet(b => b.OnlineID == testBeatmapSet.OnlineID)));
+            AddStep("delete beatmap", () => beatmaps.Delete(beatmaps.QueryBeatmapSet(b => b.OnlineID == testBeatmapSet.OnlineID)!.Value));
             addAvailabilityCheckStep("state not downloaded", BeatmapAvailability.NotDownloaded);
 
-            AddStep("undelete beatmap", () => beatmaps.Undelete(beatmaps.QueryBeatmapSet(b => b.OnlineID == testBeatmapSet.OnlineID)));
+            AddStep("undelete beatmap", () => beatmaps.Undelete(beatmaps.QueryBeatmapSet(b => b.OnlineID == testBeatmapSet.OnlineID)!.Value));
             addAvailabilityCheckStep("state locally available", BeatmapAvailability.LocallyAvailable);
         }
 
@@ -154,7 +153,6 @@ namespace osu.Game.Tests.Online
                 Debug.Assert(info.BeatmapSet != null);
 
                 info.BeatmapSet.Beatmaps.Add(info);
-                info.BeatmapSet.Metadata = info.Metadata;
                 info.MD5Hash = stream.ComputeMD5Hash();
                 info.Hash = stream.ComputeSHA2Hash();
             }
@@ -168,22 +166,22 @@ namespace osu.Game.Tests.Online
 
             public Task<ILive<BeatmapSetInfo>> CurrentImportTask { get; private set; }
 
-            public TestBeatmapManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, IAPIProvider api, [NotNull] AudioManager audioManager, IResourceStore<byte[]> resources, GameHost host = null, WorkingBeatmap defaultBeatmap = null)
+            public TestBeatmapManager(Storage storage, RealmContextFactory contextFactory, RulesetStore rulesets, IAPIProvider api, [NotNull] AudioManager audioManager, IResourceStore<byte[]> resources, GameHost host = null, WorkingBeatmap defaultBeatmap = null)
                 : base(storage, contextFactory, rulesets, api, audioManager, resources, host, defaultBeatmap)
             {
             }
 
-            protected override BeatmapModelManager CreateBeatmapModelManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, IAPIProvider api, GameHost host)
+            protected override BeatmapModelManager CreateBeatmapModelManager(Storage storage, RealmContextFactory contextFactory, RulesetStore rulesets, BeatmapOnlineLookupQueue onlineLookupQueue)
             {
-                return new TestBeatmapModelManager(this, storage, contextFactory, rulesets, api, host);
+                return new TestBeatmapModelManager(this, storage, contextFactory, rulesets, onlineLookupQueue);
             }
 
             internal class TestBeatmapModelManager : BeatmapModelManager
             {
                 private readonly TestBeatmapManager testBeatmapManager;
 
-                public TestBeatmapModelManager(TestBeatmapManager testBeatmapManager, Storage storage, IDatabaseContextFactory databaseContextFactory, RulesetStore rulesetStore, IAPIProvider apiProvider, GameHost gameHost)
-                    : base(storage, databaseContextFactory, rulesetStore, gameHost)
+                public TestBeatmapModelManager(TestBeatmapManager testBeatmapManager, Storage storage, RealmContextFactory databaseContextFactory, RulesetStore rulesetStore, BeatmapOnlineLookupQueue beatmapOnlineLookupQueue)
+                    : base(databaseContextFactory, storage, beatmapOnlineLookupQueue)
                 {
                     this.testBeatmapManager = testBeatmapManager;
                 }
diff --git a/osu.Game.Tests/Resources/TestResources.cs b/osu.Game.Tests/Resources/TestResources.cs
index 445394fc77..d2cab09ac9 100644
--- a/osu.Game.Tests/Resources/TestResources.cs
+++ b/osu.Game.Tests/Resources/TestResources.cs
@@ -89,7 +89,7 @@ namespace osu.Game.Tests.Resources
                 // Create random metadata, then we can check if sorting works based on these
                 Artist = "Some Artist " + RNG.Next(0, 9),
                 Title = $"Some Song (set id {setId}) {Guid.NewGuid()}",
-                AuthorString = "Some Guy " + RNG.Next(0, 9),
+                Author = { Username = "Some Guy " + RNG.Next(0, 9) },
             };
 
             var beatmapSet = new BeatmapSetInfo
@@ -97,7 +97,6 @@ namespace osu.Game.Tests.Resources
                 OnlineID = setId,
                 Hash = new MemoryStream(Encoding.UTF8.GetBytes(Guid.NewGuid().ToString())).ComputeMD5Hash(),
                 DateAdded = DateTimeOffset.UtcNow,
-                Metadata = metadata
             };
 
             foreach (var b in getBeatmaps(difficultyCount ?? RNG.Next(1, 20)))
@@ -131,10 +130,10 @@ namespace osu.Game.Tests.Resources
                         StarRating = diff,
                         Length = length,
                         BPM = bpm,
+                        Hash = Guid.NewGuid().ToString().ComputeMD5Hash(),
                         Ruleset = rulesetInfo,
-                        RulesetID = rulesetInfo.ID ?? -1,
                         Metadata = metadata,
-                        BaseDifficulty = new BeatmapDifficulty
+                        Difficulty = new BeatmapDifficulty
                         {
                             OverallDifficulty = diff,
                         }
@@ -166,7 +165,6 @@ namespace osu.Game.Tests.Resources
             },
             BeatmapInfo = beatmap,
             Ruleset = beatmap.Ruleset,
-            RulesetID = beatmap.Ruleset.ID ?? 0,
             Mods = new Mod[] { new TestModHardRock(), new TestModDoubleTime() },
             TotalScore = 2845370,
             Accuracy = 0.95,
diff --git a/osu.Game.Tests/Scores/IO/ImportScoreTest.cs b/osu.Game.Tests/Scores/IO/ImportScoreTest.cs
index bbc92b7817..dd12c94855 100644
--- a/osu.Game.Tests/Scores/IO/ImportScoreTest.cs
+++ b/osu.Game.Tests/Scores/IO/ImportScoreTest.cs
@@ -8,8 +8,8 @@ using System.Linq;
 using System.Threading.Tasks;
 using NUnit.Framework;
 using osu.Framework.Allocation;
+using osu.Framework.Extensions;
 using osu.Framework.Platform;
-using osu.Game.Beatmaps;
 using osu.Game.IO.Archives;
 using osu.Game.Online.API.Requests.Responses;
 using osu.Game.Rulesets.Mods;
@@ -17,6 +17,8 @@ using osu.Game.Rulesets.Osu;
 using osu.Game.Rulesets.Osu.Mods;
 using osu.Game.Rulesets.Scoring;
 using osu.Game.Scoring;
+using osu.Game.Tests.Beatmaps.IO;
+using osu.Game.Tests.Resources;
 
 namespace osu.Game.Tests.Scores.IO
 {
@@ -31,6 +33,8 @@ namespace osu.Game.Tests.Scores.IO
                 {
                     var osu = LoadOsuIntoHost(host, true);
 
+                    var beatmap = BeatmapImportHelper.LoadOszIntoOsu(osu, TestResources.GetQuickTestBeatmapForImport()).GetResultSafely();
+
                     var toImport = new ScoreInfo
                     {
                         Rank = ScoreRank.B,
@@ -41,6 +45,8 @@ namespace osu.Game.Tests.Scores.IO
                         User = new APIUser { Username = "Test user" },
                         Date = DateTimeOffset.Now,
                         OnlineID = 12345,
+                        Ruleset = new OsuRuleset().RulesetInfo,
+                        BeatmapInfo = beatmap.Beatmaps.First()
                     };
 
                     var imported = await LoadScoreIntoOsu(osu, toImport);
@@ -49,7 +55,6 @@ namespace osu.Game.Tests.Scores.IO
                     Assert.AreEqual(toImport.TotalScore, imported.TotalScore);
                     Assert.AreEqual(toImport.Accuracy, imported.Accuracy);
                     Assert.AreEqual(toImport.MaxCombo, imported.MaxCombo);
-                    Assert.AreEqual(toImport.Combo, imported.Combo);
                     Assert.AreEqual(toImport.User.Username, imported.User.Username);
                     Assert.AreEqual(toImport.Date, imported.Date);
                     Assert.AreEqual(toImport.OnlineID, imported.OnlineID);
@@ -70,8 +75,13 @@ namespace osu.Game.Tests.Scores.IO
                 {
                     var osu = LoadOsuIntoHost(host, true);
 
+                    var beatmap = BeatmapImportHelper.LoadOszIntoOsu(osu, TestResources.GetQuickTestBeatmapForImport()).GetResultSafely();
+
                     var toImport = new ScoreInfo
                     {
+                        User = new APIUser { Username = "Test user" },
+                        BeatmapInfo = beatmap.Beatmaps.First(),
+                        Ruleset = new OsuRuleset().RulesetInfo,
                         Mods = new Mod[] { new OsuModHardRock(), new OsuModDoubleTime() },
                     };
 
@@ -96,8 +106,13 @@ namespace osu.Game.Tests.Scores.IO
                 {
                     var osu = LoadOsuIntoHost(host, true);
 
+                    var beatmap = BeatmapImportHelper.LoadOszIntoOsu(osu, TestResources.GetQuickTestBeatmapForImport()).GetResultSafely();
+
                     var toImport = new ScoreInfo
                     {
+                        User = new APIUser { Username = "Test user" },
+                        BeatmapInfo = beatmap.Beatmaps.First(),
+                        Ruleset = new OsuRuleset().RulesetInfo,
                         Statistics = new Dictionary<HitResult, int>
                         {
                             { HitResult.Perfect, 100 },
@@ -117,43 +132,6 @@ namespace osu.Game.Tests.Scores.IO
             }
         }
 
-        [Test]
-        public async Task TestImportWithDeletedBeatmapSet()
-        {
-            using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
-            {
-                try
-                {
-                    var osu = LoadOsuIntoHost(host, true);
-
-                    var toImport = new ScoreInfo
-                    {
-                        Hash = Guid.NewGuid().ToString(),
-                        Statistics = new Dictionary<HitResult, int>
-                        {
-                            { HitResult.Perfect, 100 },
-                            { HitResult.Miss, 50 }
-                        }
-                    };
-
-                    var imported = await LoadScoreIntoOsu(osu, toImport);
-
-                    var beatmapManager = osu.Dependencies.Get<BeatmapManager>();
-                    var scoreManager = osu.Dependencies.Get<ScoreManager>();
-
-                    beatmapManager.Delete(beatmapManager.QueryBeatmapSet(s => s.Beatmaps.Any(b => b.ID == imported.BeatmapInfo.ID)));
-                    Assert.That(scoreManager.Query(s => s.Equals(imported)).DeletePending, Is.EqualTo(true));
-
-                    var secondImport = await LoadScoreIntoOsu(osu, imported);
-                    Assert.That(secondImport, Is.Null);
-                }
-                finally
-                {
-                    host.Exit();
-                }
-            }
-        }
-
         [Test]
         public async Task TestOnlineScoreIsAvailableLocally()
         {
@@ -163,12 +141,25 @@ namespace osu.Game.Tests.Scores.IO
                 {
                     var osu = LoadOsuIntoHost(host, true);
 
-                    await LoadScoreIntoOsu(osu, new ScoreInfo { OnlineID = 2 }, new TestArchiveReader());
+                    var beatmap = BeatmapImportHelper.LoadOszIntoOsu(osu, TestResources.GetQuickTestBeatmapForImport()).GetResultSafely();
+
+                    await LoadScoreIntoOsu(osu, new ScoreInfo
+                    {
+                        User = new APIUser { Username = "Test user" },
+                        BeatmapInfo = beatmap.Beatmaps.First(),
+                        Ruleset = new OsuRuleset().RulesetInfo,
+                        OnlineID = 2
+                    }, new TestArchiveReader());
 
                     var scoreManager = osu.Dependencies.Get<ScoreManager>();
 
                     // Note: A new score reference is used here since the import process mutates the original object to set an ID
-                    Assert.That(scoreManager.IsAvailableLocally(new ScoreInfo { OnlineID = 2 }));
+                    Assert.That(scoreManager.IsAvailableLocally(new ScoreInfo
+                    {
+                        User = new APIUser { Username = "Test user" },
+                        BeatmapInfo = beatmap.Beatmaps.First(),
+                        OnlineID = 2
+                    }));
                 }
                 finally
                 {
@@ -179,15 +170,13 @@ namespace osu.Game.Tests.Scores.IO
 
         public static async Task<ScoreInfo> LoadScoreIntoOsu(OsuGameBase osu, ScoreInfo score, ArchiveReader archive = null)
         {
-            var beatmapManager = osu.Dependencies.Get<BeatmapManager>();
-
-            score.BeatmapInfo ??= beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps.First();
-            score.Ruleset ??= new OsuRuleset().RulesetInfo;
+            // clone to avoid attaching the input score to realm.
+            score = score.DeepClone();
 
             var scoreManager = osu.Dependencies.Get<ScoreManager>();
             await scoreManager.Import(score, archive);
 
-            return scoreManager.GetAllUsableScores().FirstOrDefault();
+            return scoreManager.Query(_ => true);
         }
 
         internal class TestArchiveReader : ArchiveReader
diff --git a/osu.Game.Tests/Scores/IO/TestScoreEquality.cs b/osu.Game.Tests/Scores/IO/TestScoreEquality.cs
index 42fcb3acab..f898774ce6 100644
--- a/osu.Game.Tests/Scores/IO/TestScoreEquality.cs
+++ b/osu.Game.Tests/Scores/IO/TestScoreEquality.cs
@@ -1,6 +1,7 @@
 // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
 // See the LICENCE file in the repository root for full licence text.
 
+using System;
 using NUnit.Framework;
 using osu.Game.Scoring;
 
@@ -29,8 +30,8 @@ namespace osu.Game.Tests.Scores.IO
         [Test]
         public void TestNonMatchingByPrimaryKey()
         {
-            ScoreInfo score1 = new ScoreInfo { ID = 1 };
-            ScoreInfo score2 = new ScoreInfo { ID = 2 };
+            ScoreInfo score1 = new ScoreInfo { ID = Guid.NewGuid() };
+            ScoreInfo score2 = new ScoreInfo { ID = Guid.NewGuid() };
 
             Assert.That(score1, Is.Not.EqualTo(score2));
         }
@@ -38,8 +39,10 @@ namespace osu.Game.Tests.Scores.IO
         [Test]
         public void TestMatchingByPrimaryKey()
         {
-            ScoreInfo score1 = new ScoreInfo { ID = 1 };
-            ScoreInfo score2 = new ScoreInfo { ID = 1 };
+            Guid id = Guid.NewGuid();
+
+            ScoreInfo score1 = new ScoreInfo { ID = id };
+            ScoreInfo score2 = new ScoreInfo { ID = id };
 
             Assert.That(score1, Is.EqualTo(score2));
         }
diff --git a/osu.Game.Tests/Skins/TestSceneBeatmapSkinResources.cs b/osu.Game.Tests/Skins/TestSceneBeatmapSkinResources.cs
index c20ab84a68..fe0423dcfc 100644
--- a/osu.Game.Tests/Skins/TestSceneBeatmapSkinResources.cs
+++ b/osu.Game.Tests/Skins/TestSceneBeatmapSkinResources.cs
@@ -26,8 +26,12 @@ namespace osu.Game.Tests.Skins
         private void load()
         {
             var imported = beatmaps.Import(new ZipArchiveReader(TestResources.OpenResource("Archives/ogg-beatmap.osz"))).GetResultSafely();
-            beatmap = beatmaps.GetWorkingBeatmap(imported.Value.Beatmaps[0]);
-            beatmap.LoadTrack();
+
+            imported?.PerformRead(s =>
+            {
+                beatmap = beatmaps.GetWorkingBeatmap(s.Beatmaps[0]);
+                beatmap.LoadTrack();
+            });
         }
 
         [Test]
diff --git a/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs b/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs
index 5b2cf877ba..4ab4c08353 100644
--- a/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs
+++ b/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs
@@ -50,6 +50,7 @@ namespace osu.Game.Tests.Visual.Background
             Dependencies.Cache(rulesets = new RulesetStore(ContextFactory));
             Dependencies.Cache(manager = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, audio, Resources, host, Beatmap.Default));
             Dependencies.Cache(new OsuConfigManager(LocalStorage));
+            Dependencies.Cache(ContextFactory);
 
             manager.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely();
 
@@ -387,6 +388,9 @@ namespace osu.Game.Tests.Visual.Background
                 while (BlockLoad && !token.IsCancellationRequested)
                     Thread.Sleep(1);
 
+                if (!LoadedBeatmapSuccessfully)
+                    return;
+
                 StoryboardEnabled = config.GetBindable<bool>(OsuSetting.ShowStoryboard);
                 DrawableRuleset.IsPaused.BindTo(IsPaused);
             }
diff --git a/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapCard.cs b/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapCard.cs
index 7b5e1f4ec7..94b693363a 100644
--- a/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapCard.cs
+++ b/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapCard.cs
@@ -204,7 +204,7 @@ namespace osu.Game.Tests.Visual.Beatmaps
             {
                 var beatmap = beatmaps.QueryBeatmapSet(b => b.OnlineID == online_id);
 
-                if (beatmap != null) beatmaps.Delete(beatmap);
+                if (beatmap != null) beatmaps.Delete(beatmap.Value);
             });
         }
 
diff --git a/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapCardDifficultyList.cs b/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapCardDifficultyList.cs
index aec75884d6..e6fb4372ff 100644
--- a/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapCardDifficultyList.cs
+++ b/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapCardDifficultyList.cs
@@ -6,7 +6,6 @@ using osu.Framework.Graphics;
 using osu.Framework.Graphics.Containers;
 using osu.Framework.Graphics.Shapes;
 using osu.Game.Beatmaps.Drawables.Cards;
-using osu.Game.Graphics;
 using osu.Game.Online.API.Requests.Responses;
 using osu.Game.Overlays;
 
@@ -18,7 +17,7 @@ namespace osu.Game.Tests.Visual.Beatmaps
         private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue);
 
         [BackgroundDependencyLoader]
-        private void load(OsuColour colours)
+        private void load()
         {
             var beatmapSet = new APIBeatmapSet
             {
diff --git a/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs b/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs
index d2b0f7324b..18572ac211 100644
--- a/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs
+++ b/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs
@@ -38,6 +38,7 @@ namespace osu.Game.Tests.Visual.Collections
         {
             Dependencies.Cache(rulesets = new RulesetStore(ContextFactory));
             Dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, Audio, Resources, host, Beatmap.Default));
+            Dependencies.Cache(ContextFactory);
 
             beatmapManager.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely();
 
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneComposeScreen.cs b/osu.Game.Tests/Visual/Editing/TestSceneComposeScreen.cs
index 9b8567e853..d100fba8d6 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneComposeScreen.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneComposeScreen.cs
@@ -29,9 +29,10 @@ namespace osu.Game.Tests.Visual.Editing
         [Cached]
         private EditorClipboard clipboard = new EditorClipboard();
 
-        [BackgroundDependencyLoader]
-        private void load()
+        protected override void LoadComplete()
         {
+            base.LoadComplete();
+
             Beatmap.Value = CreateWorkingBeatmap(editorBeatmap.PlayableBeatmap);
 
             Child = new ComposeScreen
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneDesignSection.cs b/osu.Game.Tests/Visual/Editing/TestSceneDesignSection.cs
index 00f2979691..10917df075 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneDesignSection.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneDesignSection.cs
@@ -11,6 +11,7 @@ using osu.Framework.Graphics.UserInterface;
 using osu.Framework.Testing;
 using osu.Game.Beatmaps;
 using osu.Game.Graphics.UserInterfaceV2;
+using osu.Game.Rulesets.Osu;
 using osu.Game.Screens.Edit;
 using osu.Game.Screens.Edit.Setup;
 using osuTK.Input;
@@ -25,7 +26,13 @@ namespace osu.Game.Tests.Visual.Editing
         [SetUpSteps]
         public void SetUp()
         {
-            AddStep("create blank beatmap", () => editorBeatmap = new EditorBeatmap(new Beatmap()));
+            AddStep("create blank beatmap", () => editorBeatmap = new EditorBeatmap(new Beatmap
+            {
+                BeatmapInfo =
+                {
+                    Ruleset = new OsuRuleset().RulesetInfo
+                }
+            }));
             AddStep("create section", () => Child = new DependencyProvidingContainer
             {
                 RelativeSizeAxes = Axes.Both,
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneDifficultySwitching.cs b/osu.Game.Tests/Visual/Editing/TestSceneDifficultySwitching.cs
index 516305079b..243bb71e26 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneDifficultySwitching.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneDifficultySwitching.cs
@@ -33,7 +33,7 @@ namespace osu.Game.Tests.Visual.Editing
 
         public override void SetUpSteps()
         {
-            AddStep("import test beatmap", () => importedBeatmapSet = ImportBeatmapTest.LoadOszIntoOsu(game, virtualTrack: true).GetResultSafely());
+            AddStep("import test beatmap", () => importedBeatmapSet = BeatmapImportHelper.LoadOszIntoOsu(game, virtualTrack: true).GetResultSafely());
             base.SetUpSteps();
         }
 
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs b/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs
index d1efd22d6f..0d9e06e471 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs
@@ -8,6 +8,7 @@ using osu.Framework.Graphics.Shapes;
 using osu.Game.Beatmaps.ControlPoints;
 using osu.Game.Rulesets.Edit;
 using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Osu;
 using osu.Game.Rulesets.Osu.Beatmaps;
 using osu.Game.Screens.Edit;
 using osu.Game.Screens.Edit.Compose.Components;
@@ -29,7 +30,13 @@ namespace osu.Game.Tests.Visual.Editing
 
         public TestSceneDistanceSnapGrid()
         {
-            editorBeatmap = new EditorBeatmap(new OsuBeatmap());
+            editorBeatmap = new EditorBeatmap(new OsuBeatmap
+            {
+                BeatmapInfo =
+                {
+                    Ruleset = new OsuRuleset().RulesetInfo
+                }
+            });
             editorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = beat_length });
         }
 
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs
index db20d3c7ba..2386446e96 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs
@@ -49,8 +49,7 @@ namespace osu.Game.Tests.Visual.Editing
         public void TestCreateNewBeatmap()
         {
             AddStep("save beatmap", () => Editor.Save());
-            AddAssert("new beatmap persisted", () => EditorBeatmap.BeatmapInfo.IsManaged);
-            AddAssert("new beatmap in database", () => beatmapManager.QueryBeatmapSet(s => s.ID == EditorBeatmap.BeatmapInfo.BeatmapSet.ID)?.DeletePending == false);
+            AddAssert("new beatmap in database", () => beatmapManager.QueryBeatmapSet(s => s.ID == EditorBeatmap.BeatmapInfo.BeatmapSet.ID)?.Value.DeletePending == false);
         }
 
         [Test]
@@ -66,7 +65,7 @@ namespace osu.Game.Tests.Visual.Editing
             });
 
             AddUntilStep("wait for exit", () => !Editor.IsCurrentScreen());
-            AddAssert("new beatmap not persisted", () => beatmapManager.QueryBeatmapSet(s => s.ID == editorBeatmap.BeatmapInfo.BeatmapSet.ID)?.DeletePending == true);
+            AddAssert("new beatmap not persisted", () => beatmapManager.QueryBeatmapSet(s => s.ID == editorBeatmap.BeatmapInfo.BeatmapSet.ID)?.Value.DeletePending == true);
         }
 
         [Test]
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorClock.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorClock.cs
index 0abf0c47f8..4b9be77471 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneEditorClock.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorClock.cs
@@ -2,7 +2,6 @@
 // See the LICENCE file in the repository root for full licence text.
 
 using NUnit.Framework;
-using osu.Framework.Allocation;
 using osu.Framework.Graphics;
 using osu.Framework.Graphics.Containers;
 using osu.Game.Rulesets.Osu;
@@ -37,9 +36,10 @@ namespace osu.Game.Tests.Visual.Editing
             });
         }
 
-        [BackgroundDependencyLoader]
-        private void load()
+        protected override void LoadComplete()
         {
+            base.LoadComplete();
+
             Beatmap.Value = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo);
             // ensure that music controller does not change this beatmap due to it
             // completing naturally as part of the test.
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorSeekSnapping.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorSeekSnapping.cs
index 3a19eabe81..863f42520b 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneEditorSeekSnapping.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorSeekSnapping.cs
@@ -2,7 +2,6 @@
 // See the LICENCE file in the repository root for full licence text.
 
 using NUnit.Framework;
-using osu.Framework.Allocation;
 using osu.Framework.Extensions.Color4Extensions;
 using osu.Framework.Graphics;
 using osu.Framework.Graphics.Containers;
@@ -23,9 +22,10 @@ namespace osu.Game.Tests.Visual.Editing
             BeatDivisor.Value = 4;
         }
 
-        [BackgroundDependencyLoader]
-        private void load()
+        protected override void LoadComplete()
         {
+            base.LoadComplete();
+
             var testBeatmap = new Beatmap
             {
                 ControlPointInfo = new ControlPointInfo(),
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorTestGameplay.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorTestGameplay.cs
index 6d48ef3ba7..bb630e5d5c 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneEditorTestGameplay.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorTestGameplay.cs
@@ -39,7 +39,7 @@ namespace osu.Game.Tests.Visual.Editing
 
         public override void SetUpSteps()
         {
-            AddStep("import test beatmap", () => importedBeatmapSet = ImportBeatmapTest.LoadOszIntoOsu(game).GetResultSafely());
+            AddStep("import test beatmap", () => importedBeatmapSet = BeatmapImportHelper.LoadOszIntoOsu(game).GetResultSafely());
             base.SetUpSteps();
         }
 
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneHitObjectComposer.cs b/osu.Game.Tests/Visual/Editing/TestSceneHitObjectComposer.cs
index eee0d6672c..145d738f60 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneHitObjectComposer.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneHitObjectComposer.cs
@@ -39,9 +39,16 @@ namespace osu.Game.Tests.Visual.Editing
             {
                 Beatmap.Value = CreateWorkingBeatmap(new Beatmap
                 {
+                    BeatmapInfo =
+                    {
+                        Ruleset = new OsuRuleset().RulesetInfo
+                    },
                     HitObjects = new List<HitObject>
                     {
-                        new HitCircle { Position = new Vector2(256, 192), Scale = 0.5f },
+                        new HitCircle
+                        {
+                            Position = new Vector2(256, 192), Scale = 0.5f
+                        },
                         new HitCircle { Position = new Vector2(344, 148), Scale = 0.5f },
                         new Slider
                         {
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneMetadataSection.cs b/osu.Game.Tests/Visual/Editing/TestSceneMetadataSection.cs
index 4621436cc6..4ecfb0975b 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneMetadataSection.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneMetadataSection.cs
@@ -5,6 +5,7 @@ using NUnit.Framework;
 using osu.Framework.Allocation;
 using osu.Game.Beatmaps;
 using osu.Game.Graphics.UserInterfaceV2;
+using osu.Game.Rulesets.Osu;
 using osu.Game.Screens.Edit;
 using osu.Game.Screens.Edit.Setup;
 
@@ -13,7 +14,13 @@ namespace osu.Game.Tests.Visual.Editing
     public class TestSceneMetadataSection : OsuTestScene
     {
         [Cached]
-        private EditorBeatmap editorBeatmap = new EditorBeatmap(new Beatmap());
+        private EditorBeatmap editorBeatmap = new EditorBeatmap(new Beatmap
+        {
+            BeatmapInfo =
+            {
+                Ruleset = new OsuRuleset().RulesetInfo
+            },
+        });
 
         private TestMetadataSection metadataSection;
 
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneSetupScreen.cs b/osu.Game.Tests/Visual/Editing/TestSceneSetupScreen.cs
index 03e78ce854..2f6cf46b21 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneSetupScreen.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneSetupScreen.cs
@@ -29,7 +29,13 @@ namespace osu.Game.Tests.Visual.Editing
 
         public TestSceneSetupScreen()
         {
-            editorBeatmap = new EditorBeatmap(new OsuBeatmap());
+            editorBeatmap = new EditorBeatmap(new OsuBeatmap
+            {
+                BeatmapInfo =
+                {
+                    Ruleset = new OsuRuleset().RulesetInfo
+                }
+            });
         }
 
         [Test]
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneTimingScreen.cs b/osu.Game.Tests/Visual/Editing/TestSceneTimingScreen.cs
index 4bbffbdc7a..17b8189fc7 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneTimingScreen.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneTimingScreen.cs
@@ -29,9 +29,10 @@ namespace osu.Game.Tests.Visual.Editing
             editorBeatmap = new EditorBeatmap(CreateBeatmap(new OsuRuleset().RulesetInfo));
         }
 
-        [BackgroundDependencyLoader]
-        private void load()
+        protected override void LoadComplete()
         {
+            base.LoadComplete();
+
             Beatmap.Value = CreateWorkingBeatmap(editorBeatmap.PlayableBeatmap);
             Beatmap.Disabled = true;
 
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneAllRulesetPlayers.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneAllRulesetPlayers.cs
index c5ab3974a4..e10ef57a25 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneAllRulesetPlayers.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneAllRulesetPlayers.cs
@@ -25,7 +25,7 @@ namespace osu.Game.Tests.Visual.Gameplay
         protected OsuConfigManager Config { get; private set; }
 
         [BackgroundDependencyLoader]
-        private void load(RulesetStore rulesets)
+        private void load()
         {
             Dependencies.Cache(Config = new OsuConfigManager(LocalStorage));
             Config.GetBindable<double>(OsuSetting.DimLevel).Value = 1.0;
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySampleTriggerSource.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySampleTriggerSource.cs
index fccc1a377c..ac39395567 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySampleTriggerSource.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySampleTriggerSource.cs
@@ -29,7 +29,7 @@ namespace osu.Game.Tests.Visual.Gameplay
             {
                 BeatmapInfo = new BeatmapInfo
                 {
-                    BaseDifficulty = new BeatmapDifficulty { CircleSize = 6, SliderMultiplier = 3 },
+                    Difficulty = new BeatmapDifficulty { CircleSize = 6, SliderMultiplier = 3 },
                     Ruleset = ruleset
                 }
             };
diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs
index cf5aadde6d..a4a4f351ec 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs
@@ -237,7 +237,7 @@ namespace osu.Game.Tests.Visual.Gameplay
             createPlayerTest(false, r =>
             {
                 var beatmap = createTestBeatmap(r);
-                beatmap.BeatmapInfo.OnlineID = null;
+                beatmap.BeatmapInfo.OnlineID = -1;
                 return beatmap;
             });
 
@@ -255,7 +255,15 @@ namespace osu.Game.Tests.Visual.Gameplay
         {
             prepareTestAPI(true);
 
-            createPlayerTest(false, createRuleset: () => new OsuRuleset { RulesetInfo = { OnlineID = rulesetId ?? -1 } });
+            createPlayerTest(false, createRuleset: () => new OsuRuleset
+            {
+                RulesetInfo =
+                {
+                    Name = "custom",
+                    ShortName = $"custom{rulesetId}",
+                    OnlineID = rulesetId ?? -1
+                }
+            });
 
             AddUntilStep("wait for token request", () => Player.TokenCreationRequested);
 
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs
index 3168c4b94e..8199389b36 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs
@@ -6,16 +6,18 @@ using NUnit.Framework;
 using osu.Framework.Graphics;
 using osu.Game.Online;
 using osu.Game.Online.API.Requests.Responses;
-using osu.Game.Scoring;
 using osu.Framework.Allocation;
 using osu.Framework.Bindables;
 using osu.Framework.Extensions;
 using osu.Framework.Testing;
+using osu.Game.Beatmaps;
 using osu.Game.Database;
 using osu.Game.Graphics.UserInterface;
 using osu.Game.Rulesets;
 using osu.Game.Rulesets.Osu;
+using osu.Game.Scoring;
 using osu.Game.Screens.Ranking;
+using osu.Game.Tests.Resources;
 using osuTK.Input;
 using APIUser = osu.Game.Online.API.Requests.Responses.APIUser;
 
@@ -29,6 +31,18 @@ namespace osu.Game.Tests.Visual.Gameplay
 
         private TestReplayDownloadButton downloadButton;
 
+        [Resolved]
+        private BeatmapManager beatmapManager { get; set; }
+
+        [Resolved]
+        private ScoreManager scoreManager { get; set; }
+
+        [BackgroundDependencyLoader]
+        private void load()
+        {
+            beatmapManager.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely();
+        }
+
         [Test]
         public void TestDisplayStates()
         {
@@ -115,9 +129,6 @@ namespace osu.Game.Tests.Visual.Gameplay
             AddAssert("button is not enabled", () => !downloadButton.ChildrenOfType<DownloadButton>().First().Enabled.Value);
         }
 
-        [Resolved]
-        private ScoreManager scoreManager { get; set; }
-
         [Test]
         public void TestScoreImportThenDelete()
         {
@@ -176,7 +187,7 @@ namespace osu.Game.Tests.Visual.Gameplay
                     Id = 39828,
                     Username = @"WubWoofWolf",
                 }
-            }.CreateScoreInfo(rulesets, CreateBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo);
+            }.CreateScoreInfo(rulesets, beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps.First());
         }
 
         private class TestReplayDownloadButton : ReplayDownloadButton
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs
index e6361a15d7..4eab1a21da 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs
@@ -64,7 +64,11 @@ namespace osu.Game.Tests.Visual.Gameplay
                                 Recorder = recorder = new TestReplayRecorder(new Score
                                 {
                                     Replay = replay,
-                                    ScoreInfo = { BeatmapInfo = gameplayState.Beatmap.BeatmapInfo }
+                                    ScoreInfo =
+                                    {
+                                        BeatmapInfo = gameplayState.Beatmap.BeatmapInfo,
+                                        Ruleset = new OsuRuleset().RulesetInfo,
+                                    }
                                 })
                                 {
                                     ScreenSpaceToGamefield = pos => recordingManager.ToLocalSpace(pos),
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs
index 242eca0bbc..8b7e1a1d85 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs
@@ -61,8 +61,8 @@ namespace osu.Game.Tests.Visual.Gameplay
 
             AddStep("import beatmap", () =>
             {
-                importedBeatmap = ImportBeatmapTest.LoadOszIntoOsu(game, virtualTrack: true).GetResultSafely();
-                importedBeatmapId = importedBeatmap.Beatmaps.First(b => b.RulesetID == 0).OnlineID ?? -1;
+                importedBeatmap = BeatmapImportHelper.LoadOszIntoOsu(game, virtualTrack: true).GetResultSafely();
+                importedBeatmapId = importedBeatmap.Beatmaps.First(b => b.RulesetID == 0).OnlineID;
             });
         }
 
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs
index f7e9a1fe16..4790bd44db 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs
@@ -372,7 +372,14 @@ namespace osu.Game.Tests.Visual.Gameplay
         internal class TestReplayRecorder : ReplayRecorder<TestAction>
         {
             public TestReplayRecorder()
-                : base(new Score { ScoreInfo = { BeatmapInfo = new BeatmapInfo() } })
+                : base(new Score
+                {
+                    ScoreInfo =
+                    {
+                        BeatmapInfo = new BeatmapInfo(),
+                        Ruleset = new OsuRuleset().RulesetInfo,
+                    }
+                })
             {
             }
 
diff --git a/osu.Game.Tests/Visual/Menus/TestSceneMusicActionHandling.cs b/osu.Game.Tests/Visual/Menus/TestSceneMusicActionHandling.cs
index ee9363fa12..3ebc64cd0b 100644
--- a/osu.Game.Tests/Visual/Menus/TestSceneMusicActionHandling.cs
+++ b/osu.Game.Tests/Visual/Menus/TestSceneMusicActionHandling.cs
@@ -39,7 +39,10 @@ namespace osu.Game.Tests.Visual.Menus
             AddStep("import beatmap with track", () =>
             {
                 var setWithTrack = Game.BeatmapManager.Import(new ImportTask(TestResources.GetTestBeatmapForImport())).GetResultSafely();
-                Beatmap.Value = Game.BeatmapManager.GetWorkingBeatmap(setWithTrack.Value.Beatmaps.First());
+                setWithTrack?.PerformRead(s =>
+                {
+                    Beatmap.Value = Game.BeatmapManager.GetWorkingBeatmap(s.Beatmaps.First());
+                });
             });
 
             AddStep("bind to track change", () =>
diff --git a/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs b/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs
index c4d7bd7e6a..d4282ff21e 100644
--- a/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs
@@ -49,6 +49,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
         {
             Dependencies.Cache(rulesets = new RulesetStore(ContextFactory));
             Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, audio, Resources, host, Beatmap.Default));
+            Dependencies.Cache(ContextFactory);
         }
 
         public override void SetUpSteps()
@@ -58,7 +59,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
             AddStep("import beatmap", () =>
             {
                 beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely();
-                importedSet = beatmaps.GetAllUsableBeatmapSetsEnumerable(IncludedDetails.All).First();
+                importedSet = beatmaps.GetAllUsableBeatmapSets().First();
                 InitialBeatmap = importedSet.Beatmaps.First(b => b.RulesetID == 0);
                 OtherBeatmap = importedSet.Beatmaps.Last(b => b.RulesetID == 0);
             });
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs
index 147bbf2626..99c867b014 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs
@@ -17,7 +17,7 @@ using osu.Game.Beatmaps;
 using osu.Game.Beatmaps.Drawables;
 using osu.Game.Database;
 using osu.Game.Graphics.Containers;
-using osu.Game.Online.API.Requests.Responses;
+using osu.Game.Models;
 using osu.Game.Online.Rooms;
 using osu.Game.Rulesets;
 using osu.Game.Rulesets.Osu;
@@ -45,6 +45,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
         {
             Dependencies.Cache(rulesets = new RulesetStore(ContextFactory));
             Dependencies.Cache(manager = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, audio, Resources, host, Beatmap.Default));
+            Dependencies.Cache(ContextFactory);
         }
 
         [Test]
@@ -153,17 +154,20 @@ namespace osu.Game.Tests.Visual.Multiplayer
         public void TestDownloadButtonHiddenWhenBeatmapExists()
         {
             var beatmap = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo;
+            ILive<BeatmapSetInfo> imported = null;
 
-            AddStep("import beatmap", () => manager.Import(beatmap.BeatmapSet).WaitSafely());
+            Debug.Assert(beatmap.BeatmapSet != null);
 
-            createPlaylistWithBeatmaps(beatmap);
+            AddStep("import beatmap", () => imported = manager.Import(beatmap.BeatmapSet).GetResultSafely());
+
+            createPlaylistWithBeatmaps(() => imported.PerformRead(s => s.Beatmaps.Detach()));
 
             assertDownloadButtonVisible(false);
 
-            AddStep("delete beatmap set", () => manager.Delete(manager.QueryBeatmapSets(_ => true).Single()));
+            AddStep("delete beatmap set", () => imported.PerformWrite(s => s.DeletePending = true));
             assertDownloadButtonVisible(true);
 
-            AddStep("undelete beatmap set", () => manager.Undelete(manager.QueryBeatmapSets(_ => true).Single()));
+            AddStep("undelete beatmap set", () => imported.PerformWrite(s => s.DeletePending = false));
             assertDownloadButtonVisible(false);
 
             void assertDownloadButtonVisible(bool visible) => AddUntilStep($"download button {(visible ? "shown" : "hidden")}",
@@ -179,7 +183,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
             var byChecksum = CreateAPIBeatmap();
             byChecksum.Checksum = "1337"; // Some random checksum that does not exist locally.
 
-            createPlaylistWithBeatmaps(byOnlineId, byChecksum);
+            createPlaylistWithBeatmaps(() => new[] { byOnlineId, byChecksum });
 
             AddAssert("download buttons shown", () => playlist.ChildrenOfType<BeatmapDownloadButton>().All(d => d.IsPresent));
         }
@@ -193,7 +197,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
 
             beatmap.BeatmapSet.HasExplicitContent = true;
 
-            createPlaylistWithBeatmaps(beatmap);
+            createPlaylistWithBeatmaps(() => new[] { beatmap });
         }
 
         [Test]
@@ -305,7 +309,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
                                     Metadata = new BeatmapMetadata
                                     {
                                         Artist = "Artist",
-                                        Author = new APIUser { Username = "Creator name here" },
+                                        Author = new RealmUser { Username = "Creator name here" },
                                         Title = "Long title used to check background colour",
                                     },
                                     BeatmapSet = new BeatmapSetInfo()
@@ -325,7 +329,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
             AddUntilStep("wait for items to load", () => playlist.ItemMap.Values.All(i => i.IsLoaded));
         }
 
-        private void createPlaylistWithBeatmaps(params IBeatmapInfo[] beatmaps)
+        private void createPlaylistWithBeatmaps(Func<IEnumerable<IBeatmapInfo>> beatmaps)
         {
             AddStep("create playlist", () =>
             {
@@ -338,7 +342,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
 
                 int index = 0;
 
-                foreach (var b in beatmaps)
+                foreach (var b in beatmaps())
                 {
                     playlist.Items.Add(new PlaylistItem
                     {
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs
index 61058bc87a..9d67742e4d 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs
@@ -47,9 +47,9 @@ namespace osu.Game.Tests.Visual.Multiplayer
         [BackgroundDependencyLoader]
         private void load()
         {
-            importedSet = ImportBeatmapTest.LoadOszIntoOsu(game, virtualTrack: true).GetResultSafely();
+            importedSet = BeatmapImportHelper.LoadOszIntoOsu(game, virtualTrack: true).GetResultSafely();
             importedBeatmap = importedSet.Beatmaps.First(b => b.RulesetID == 0);
-            importedBeatmapId = importedBeatmap.OnlineID ?? -1;
+            importedBeatmapId = importedBeatmap.OnlineID;
         }
 
         [SetUp]
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs
index 3d8c5298dc..373b165acc 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs
@@ -62,7 +62,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
         private void load(GameHost host, AudioManager audio)
         {
             Dependencies.Cache(rulesets = new RulesetStore(ContextFactory));
-            Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, audio, Resources, host, Beatmap.Default));
+            Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, ContextFactory, rulesets, API, audio, Resources, host, Beatmap.Default));
+            Dependencies.Cache(ContextFactory);
         }
 
         public override void SetUpSteps()
@@ -72,7 +73,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
             AddStep("import beatmap", () =>
             {
                 beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely();
-                importedSet = beatmaps.GetAllUsableBeatmapSetsEnumerable(IncludedDetails.All).First();
+                importedSet = beatmaps.GetAllUsableBeatmapSets().First();
             });
 
             AddStep("load multiplayer", () => LoadScreen(multiplayerComponents = new TestMultiplayerComponents()));
@@ -588,7 +589,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
             AddStep("restore beatmap", () =>
             {
                 beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely();
-                importedSet = beatmaps.GetAllUsableBeatmapSetsEnumerable(IncludedDetails.All).First();
+                importedSet = beatmaps.GetAllUsableBeatmapSets().First();
             });
 
             AddUntilStep("play started", () => multiplayerComponents.CurrentScreen is SpectatorScreen);
@@ -827,7 +828,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
             AddStep("join other user", () => client.AddUser(new APIUser { Id = 1234 }));
             AddStep("add item as other user", () => client.AddUserPlaylistItem(1234, new MultiplayerPlaylistItem(new PlaylistItem
             {
-                BeatmapID = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.RulesetID == 0)).BeatmapInfo.OnlineID ?? -1
+                BeatmapID = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.RulesetID == 0)).BeatmapInfo.OnlineID
             })));
 
             AddUntilStep("item arrived in playlist", () => client.Room?.Playlist.Count == 2);
@@ -858,7 +859,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
             AddStep("join other user", () => client.AddUser(new APIUser { Id = 1234 }));
             AddStep("add item as other user", () => client.AddUserPlaylistItem(1234, new MultiplayerPlaylistItem(new PlaylistItem
             {
-                BeatmapID = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.RulesetID == 0)).BeatmapInfo.OnlineID ?? -1
+                BeatmapID = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.RulesetID == 0)).BeatmapInfo.OnlineID
             })));
 
             AddUntilStep("item arrived in playlist", () => client.Room?.Playlist.Count == 2);
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs
index 07a8ef66e1..9b8e67b07a 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs
@@ -58,7 +58,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
 
                 foreach (int user in users)
                 {
-                    SpectatorClient.StartPlay(user, Beatmap.Value.BeatmapInfo.OnlineID ?? 0);
+                    SpectatorClient.StartPlay(user, Beatmap.Value.BeatmapInfo.OnlineID);
                     multiplayerUsers.Add(OnlinePlayDependencies.Client.AddUser(new APIUser { Id = user }, true));
                 }
 
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboardTeams.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboardTeams.cs
index 1237a21e94..8a78c12042 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboardTeams.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboardTeams.cs
@@ -62,7 +62,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
 
                 foreach (int user in users)
                 {
-                    SpectatorClient.StartPlay(user, Beatmap.Value.BeatmapInfo.OnlineID ?? 0);
+                    SpectatorClient.StartPlay(user, Beatmap.Value.BeatmapInfo.OnlineID);
                     var roomUser = OnlinePlayDependencies.Client.AddUser(new APIUser { Id = user }, true);
 
                     roomUser.MatchState = new TeamVersusUserState
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs
index bd4b38b9c0..15ebe0ee00 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs
@@ -44,6 +44,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
         {
             Dependencies.Cache(rulesets = new RulesetStore(ContextFactory));
             Dependencies.Cache(manager = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, audio, Resources, host, Beatmap.Default));
+            Dependencies.Cache(ContextFactory);
 
             beatmaps = new List<BeatmapInfo>();
 
@@ -51,14 +52,13 @@ namespace osu.Game.Tests.Visual.Multiplayer
             {
                 Artist = "Some Artist",
                 Title = "Some Beatmap",
-                AuthorString = "Some Author"
+                Author = { Username = "Some Author" },
             };
 
             var beatmapSetInfo = new BeatmapSetInfo
             {
                 OnlineID = 10,
                 Hash = Guid.NewGuid().ToString().ComputeMD5Hash(),
-                Metadata = metadata,
                 DateAdded = DateTimeOffset.UtcNow
             };
 
@@ -71,12 +71,12 @@ namespace osu.Game.Tests.Visual.Multiplayer
 
                 var beatmap = new BeatmapInfo
                 {
-                    Ruleset = rulesets.GetRuleset(i % 4),
+                    Ruleset = rulesets.GetRuleset(i % 4) ?? throw new InvalidOperationException(),
                     OnlineID = beatmapId,
                     Length = length,
                     BPM = bpm,
                     Metadata = metadata,
-                    BaseDifficulty = new BeatmapDifficulty()
+                    Difficulty = new BeatmapDifficulty()
                 };
 
                 beatmaps.Add(beatmap);
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs
index 52e46ef5af..012a2fd960 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs
@@ -40,9 +40,11 @@ namespace osu.Game.Tests.Visual.Multiplayer
         {
             Dependencies.Cache(rulesets = new RulesetStore(ContextFactory));
             Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, audio, Resources, host, Beatmap.Default));
+            Dependencies.Cache(ContextFactory);
+
             beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely();
 
-            importedSet = beatmaps.GetAllUsableBeatmapSetsEnumerable(IncludedDetails.All).First();
+            importedSet = beatmaps.GetAllUsableBeatmapSets().First();
         }
 
         [SetUp]
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs
index 464c0ea5b6..d547b42891 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs
@@ -35,6 +35,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
         {
             Dependencies.Cache(rulesets = new RulesetStore(ContextFactory));
             Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, audio, Resources, host, Beatmap.Default));
+            Dependencies.Cache(ContextFactory);
         }
 
         [SetUp]
@@ -55,7 +56,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
             AddStep("import beatmap", () =>
             {
                 beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely();
-                importedSet = beatmaps.GetAllUsableBeatmapSetsEnumerable(IncludedDetails.All).First();
+                importedSet = beatmaps.GetAllUsableBeatmapSets().First();
                 importedBeatmap = importedSet.Beatmaps.First(b => b.RulesetID == 0);
             });
 
@@ -169,7 +170,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
         private void addItemStep(bool expired = false) => AddStep("add item", () => Client.AddPlaylistItem(new MultiplayerPlaylistItem(new PlaylistItem
         {
             Beatmap = { Value = importedBeatmap },
-            BeatmapID = importedBeatmap.OnlineID ?? -1,
+            BeatmapID = importedBeatmap.OnlineID,
             Expired = expired,
             PlayedAt = DateTimeOffset.Now
         })));
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs
index 29daff546d..965b142ed7 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs
@@ -39,7 +39,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
         private void load(GameHost host, AudioManager audio)
         {
             Dependencies.Cache(rulesets = new RulesetStore(ContextFactory));
-            Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, audio, Resources, host, Beatmap.Default));
+            Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, ContextFactory, rulesets, API, audio, Resources, host, Beatmap.Default));
+            Dependencies.Cache(ContextFactory);
         }
 
         public override void SetUpSteps()
@@ -60,7 +61,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
             AddStep("import beatmap", () =>
             {
                 beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely();
-                importedSet = beatmaps.GetAllUsableBeatmapSetsEnumerable(IncludedDetails.All).First();
+                importedSet = beatmaps.GetAllUsableBeatmapSets().First();
                 importedBeatmap = importedSet.Beatmaps.First(b => b.RulesetID == 0);
             });
 
@@ -126,7 +127,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
                 MultiplayerPlaylistItem item = new MultiplayerPlaylistItem(new PlaylistItem
                 {
                     Beatmap = { Value = importedBeatmap },
-                    BeatmapID = importedBeatmap.OnlineID ?? -1,
+                    BeatmapID = importedBeatmap.OnlineID,
                 });
 
                 Client.AddUserPlaylistItem(userId(), item);
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs
index 8f51b1e381..1c346e09d5 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs
@@ -42,6 +42,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
         {
             Dependencies.Cache(rulesets = new RulesetStore(ContextFactory));
             Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, audio, Resources, host, Beatmap.Default));
+            Dependencies.Cache(ContextFactory);
+
             beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely();
         }
 
@@ -50,7 +52,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
         {
             AvailabilityTracker.SelectedItem.BindTo(selectedItem);
 
-            importedSet = beatmaps.GetAllUsableBeatmapSetsEnumerable(IncludedDetails.All).First();
+            importedSet = beatmaps.GetAllUsableBeatmapSets().First();
             Beatmap.Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First());
             selectedItem.Value = new PlaylistItem
             {
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerResults.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerResults.cs
index 4674601f28..44a1745eee 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerResults.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerResults.cs
@@ -24,7 +24,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
 
                 PlaylistItem playlistItem = new PlaylistItem
                 {
-                    BeatmapID = beatmapInfo.OnlineID ?? -1,
+                    BeatmapID = beatmapInfo.OnlineID,
                 };
 
                 Stack.Push(screen = new MultiplayerResultsScreen(score, 1, playlistItem));
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs
index d4ff9f8c41..221732910b 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs
@@ -43,6 +43,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
         {
             Dependencies.Cache(rulesets = new RulesetStore(ContextFactory));
             Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, audio, Resources, host, Beatmap.Default));
+            Dependencies.Cache(ContextFactory);
+
             beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely();
         }
 
@@ -51,7 +53,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
         {
             AvailabilityTracker.SelectedItem.BindTo(selectedItem);
 
-            importedSet = beatmaps.GetAllUsableBeatmapSetsEnumerable(IncludedDetails.All).First();
+            importedSet = beatmaps.GetAllUsableBeatmapSets().First();
             Beatmap.Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First());
             selectedItem.Value = new PlaylistItem
             {
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerTeamResults.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerTeamResults.cs
index f5df8d7507..dfc16c44f2 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerTeamResults.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerTeamResults.cs
@@ -28,7 +28,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
 
                 PlaylistItem playlistItem = new PlaylistItem
                 {
-                    BeatmapID = beatmapInfo.OnlineID ?? -1,
+                    BeatmapID = beatmapInfo.OnlineID,
                 };
 
                 SortedDictionary<int, BindableInt> teamScores = new SortedDictionary<int, BindableInt>
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsRoomSettingsPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsRoomSettingsPlaylist.cs
index 93ccd5f1e1..e63e58824f 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsRoomSettingsPlaylist.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsRoomSettingsPlaylist.cs
@@ -12,7 +12,7 @@ using osu.Game.Beatmaps;
 using osu.Game.Beatmaps.Drawables;
 using osu.Game.Database;
 using osu.Game.Graphics.Containers;
-using osu.Game.Online.API.Requests.Responses;
+using osu.Game.Models;
 using osu.Game.Online.Rooms;
 using osu.Game.Rulesets.Osu;
 using osu.Game.Rulesets.Osu.Mods;
@@ -155,7 +155,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
                                     Metadata = new BeatmapMetadata
                                     {
                                         Artist = "Artist",
-                                        Author = new APIUser { Username = "Creator name here" },
+                                        Author = new RealmUser { Username = "Creator name here" },
                                         Title = "Long title used to check background colour",
                                     },
                                     BeatmapSet = new BeatmapSetInfo()
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsSongSelect.cs b/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsSongSelect.cs
index 08fcac125d..0b0006e437 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsSongSelect.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsSongSelect.cs
@@ -36,6 +36,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
         {
             Dependencies.Cache(rulesets = new RulesetStore(ContextFactory));
             Dependencies.Cache(manager = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, audio, Resources, host, Beatmap.Default));
+            Dependencies.Cache(ContextFactory);
 
             var beatmapSet = TestResources.CreateTestBeatmapSetInfo();
 
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs
index d20fbd3539..39cde0ad87 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs
@@ -44,6 +44,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
         {
             Dependencies.Cache(rulesets = new RulesetStore(ContextFactory));
             Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, audio, Resources, host, Beatmap.Default));
+            Dependencies.Cache(ContextFactory);
         }
 
         public override void SetUpSteps()
@@ -53,7 +54,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
             AddStep("import beatmap", () =>
             {
                 beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely();
-                importedSet = beatmaps.GetAllUsableBeatmapSetsEnumerable(IncludedDetails.All).First();
+                importedSet = beatmaps.GetAllUsableBeatmapSets().First();
             });
 
             AddStep("load multiplayer", () => LoadScreen(multiplayerComponents = new TestMultiplayerComponents()));
diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneChangeAndUseGameplayBindings.cs b/osu.Game.Tests/Visual/Navigation/TestSceneChangeAndUseGameplayBindings.cs
new file mode 100644
index 0000000000..0f314242b4
--- /dev/null
+++ b/osu.Game.Tests/Visual/Navigation/TestSceneChangeAndUseGameplayBindings.cs
@@ -0,0 +1,88 @@
+// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Linq;
+using NUnit.Framework;
+using osu.Framework.Allocation;
+using osu.Framework.Extensions;
+using osu.Framework.Input.Bindings;
+using osu.Framework.Testing;
+using osu.Game.Database;
+using osu.Game.Graphics.UserInterface;
+using osu.Game.Input.Bindings;
+using osu.Game.Overlays.Settings.Sections.Input;
+using osu.Game.Screens.Play;
+using osu.Game.Screens.Select;
+using osu.Game.Tests.Beatmaps.IO;
+using osuTK.Input;
+
+namespace osu.Game.Tests.Visual.Navigation
+{
+    public class TestSceneChangeAndUseGameplayBindings : OsuGameTestScene
+    {
+        [Test]
+        public void TestGameplayKeyBindings()
+        {
+            AddAssert("databased key is default", () => firstOsuRulesetKeyBindings.KeyCombination.Keys.SequenceEqual(new[] { InputKey.Z }));
+
+            AddStep("open settings", () => { Game.Settings.Show(); });
+
+            // Until step requires as settings has a delayed load.
+            AddUntilStep("wait for button", () => configureBindingsButton?.Enabled.Value == true);
+            AddStep("scroll to section", () => Game.Settings.SectionsContainer.ScrollTo(configureBindingsButton));
+            AddStep("press button", () => configureBindingsButton.TriggerClick());
+            AddUntilStep("wait for panel", () => keyBindingPanel?.IsLoaded == true);
+            AddUntilStep("wait for osu subsection", () => osuBindingSubsection?.IsLoaded == true);
+            AddStep("scroll to section", () => keyBindingPanel.SectionsContainer.ScrollTo(osuBindingSubsection));
+            AddWaitStep("wait for scroll to end", 3);
+            AddStep("start rebinding first osu! key", () =>
+            {
+                var button = osuBindingSubsection.ChildrenOfType<KeyBindingRow>().First();
+
+                InputManager.MoveMouseTo(button);
+                InputManager.Click(MouseButton.Left);
+            });
+
+            AddStep("Press 's'", () => InputManager.Key(Key.S));
+
+            AddUntilStep("wait for database updated", () => firstOsuRulesetKeyBindings.KeyCombination.Keys.SequenceEqual(new[] { InputKey.S }));
+
+            AddStep("close settings", () => Game.Settings.Hide());
+
+            AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely());
+            PushAndConfirm(() => new PlaySongSelect());
+
+            AddStep("enter gameplay", () => InputManager.Key(Key.Enter));
+
+            AddUntilStep("wait for gameplay", () => player?.IsBreakTime.Value == false);
+
+            AddStep("press 'z'", () => InputManager.Key(Key.Z));
+            AddAssert("key counter didn't increase", () => keyCounter.CountPresses == 0);
+
+            AddStep("press 's'", () => InputManager.Key(Key.S));
+            AddAssert("key counter did increase", () => keyCounter.CountPresses == 1);
+        }
+
+        private KeyBindingsSubsection osuBindingSubsection => keyBindingPanel
+                                                              .ChildrenOfType<VariantBindingsSubsection>()
+                                                              .FirstOrDefault(s => s.Ruleset.ShortName == "osu");
+
+        private OsuButton configureBindingsButton => Game.Settings
+                                                         .ChildrenOfType<BindingSettings>().SingleOrDefault()?
+                                                         .ChildrenOfType<OsuButton>()?
+                                                         .First(b => b.Text.ToString() == "Configure");
+
+        private KeyBindingPanel keyBindingPanel => Game.Settings
+                                                       .ChildrenOfType<KeyBindingPanel>().SingleOrDefault();
+
+        private RealmKeyBinding firstOsuRulesetKeyBindings => Game.Dependencies
+                                                                  .Get<RealmContextFactory>().Context
+                                                                  .All<RealmKeyBinding>()
+                                                                  .AsEnumerable()
+                                                                  .First(k => k.RulesetName == "osu" && k.ActionInt == 0);
+
+        private Player player => Game.ScreenStack.CurrentScreen as Player;
+
+        private KeyCounter keyCounter => player.ChildrenOfType<KeyCounter>().First();
+    }
+}
diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneMouseWheelVolumeAdjust.cs b/osu.Game.Tests/Visual/Navigation/TestSceneMouseWheelVolumeAdjust.cs
index 701ab480f6..22a00a3e5a 100644
--- a/osu.Game.Tests/Visual/Navigation/TestSceneMouseWheelVolumeAdjust.cs
+++ b/osu.Game.Tests/Visual/Navigation/TestSceneMouseWheelVolumeAdjust.cs
@@ -83,7 +83,7 @@ namespace osu.Game.Tests.Visual.Navigation
             PushAndConfirm(() => songSelect = new TestSceneScreenNavigation.TestPlaySongSelect());
             AddUntilStep("wait for song select", () => songSelect.BeatmapSetsLoaded);
 
-            AddStep("import beatmap", () => ImportBeatmapTest.LoadOszIntoOsu(Game, virtualTrack: true).WaitSafely());
+            AddStep("import beatmap", () => BeatmapImportHelper.LoadOszIntoOsu(Game, virtualTrack: true).WaitSafely());
             AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault);
             AddStep("press enter", () => InputManager.Key(Key.Enter));
 
diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneOsuGame.cs b/osu.Game.Tests/Visual/Navigation/TestSceneOsuGame.cs
index 28ff776d5f..b8d1636ea0 100644
--- a/osu.Game.Tests/Visual/Navigation/TestSceneOsuGame.cs
+++ b/osu.Game.Tests/Visual/Navigation/TestSceneOsuGame.cs
@@ -12,11 +12,9 @@ using osu.Framework.Platform;
 using osu.Game.Audio;
 using osu.Game.Beatmaps;
 using osu.Game.Configuration;
-using osu.Game.Database;
 using osu.Game.Graphics;
 using osu.Game.Input;
 using osu.Game.Input.Bindings;
-using osu.Game.IO;
 using osu.Game.Online.API;
 using osu.Game.Online.Chat;
 using osu.Game.Overlays;
@@ -58,7 +56,6 @@ namespace osu.Game.Tests.Visual.Navigation
         private IReadOnlyList<Type> requiredGameBaseDependencies => new[]
         {
             typeof(OsuGameBase),
-            typeof(DatabaseContextFactory),
             typeof(Bindable<RulesetInfo>),
             typeof(IBindable<RulesetInfo>),
             typeof(Bindable<IReadOnlyList<Mod>>),
@@ -69,7 +66,6 @@ namespace osu.Game.Tests.Visual.Navigation
             typeof(ISkinSource),
             typeof(IAPIProvider),
             typeof(RulesetStore),
-            typeof(FileStore),
             typeof(ScoreManager),
             typeof(BeatmapManager),
             typeof(IRulesetConfigCache),
diff --git a/osu.Game.Tests/Visual/Navigation/TestScenePerformFromScreen.cs b/osu.Game.Tests/Visual/Navigation/TestScenePerformFromScreen.cs
index 24f5808961..1ebceed15d 100644
--- a/osu.Game.Tests/Visual/Navigation/TestScenePerformFromScreen.cs
+++ b/osu.Game.Tests/Visual/Navigation/TestScenePerformFromScreen.cs
@@ -173,7 +173,7 @@ namespace osu.Game.Tests.Visual.Navigation
 
         private void importAndWaitForSongSelect()
         {
-            AddStep("import beatmap", () => ImportBeatmapTest.LoadQuickOszIntoOsu(Game).WaitSafely());
+            AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely());
             PushAndConfirm(() => new TestPlaySongSelect());
             AddUntilStep("beatmap updated", () => Game.Beatmap.Value.BeatmapSetInfo.OnlineID == 241526);
         }
diff --git a/osu.Game.Tests/Visual/Navigation/TestScenePresentBeatmap.cs b/osu.Game.Tests/Visual/Navigation/TestScenePresentBeatmap.cs
index 6420e7b849..f6c53e76c4 100644
--- a/osu.Game.Tests/Visual/Navigation/TestScenePresentBeatmap.cs
+++ b/osu.Game.Tests/Visual/Navigation/TestScenePresentBeatmap.cs
@@ -97,11 +97,10 @@ namespace osu.Game.Tests.Visual.Navigation
             BeatmapSetInfo imported = null;
             AddStep($"import beatmap {i}", () =>
             {
-                var difficulty = new BeatmapDifficulty();
                 var metadata = new BeatmapMetadata
                 {
                     Artist = "SomeArtist",
-                    AuthorString = "SomeAuthor",
+                    Author = { Username = "SomeAuthor" },
                     Title = $"import {i}"
                 };
 
@@ -109,25 +108,24 @@ namespace osu.Game.Tests.Visual.Navigation
                 {
                     Hash = Guid.NewGuid().ToString(),
                     OnlineID = i,
-                    Metadata = metadata,
                     Beatmaps =
                     {
                         new BeatmapInfo
                         {
                             OnlineID = i * 1024,
                             Metadata = metadata,
-                            BaseDifficulty = difficulty,
+                            Difficulty = new BeatmapDifficulty(),
                             Ruleset = ruleset ?? new OsuRuleset().RulesetInfo
                         },
                         new BeatmapInfo
                         {
                             OnlineID = i * 2048,
                             Metadata = metadata,
-                            BaseDifficulty = difficulty,
+                            Difficulty = new BeatmapDifficulty(),
                             Ruleset = ruleset ?? new OsuRuleset().RulesetInfo
                         },
                     }
-                }).GetResultSafely().Value;
+                }).GetResultSafely()?.Value;
             });
 
             AddAssert($"import {i} succeeded", () => imported != null);
diff --git a/osu.Game.Tests/Visual/Navigation/TestScenePresentScore.cs b/osu.Game.Tests/Visual/Navigation/TestScenePresentScore.cs
index 5dc1808c12..7bd8110374 100644
--- a/osu.Game.Tests/Visual/Navigation/TestScenePresentScore.cs
+++ b/osu.Game.Tests/Visual/Navigation/TestScenePresentScore.cs
@@ -8,6 +8,7 @@ using osu.Framework.Extensions;
 using osu.Framework.Screens;
 using osu.Framework.Testing;
 using osu.Game.Beatmaps;
+using osu.Game.Online.API;
 using osu.Game.Rulesets;
 using osu.Game.Rulesets.Mania;
 using osu.Game.Rulesets.Osu;
@@ -28,37 +29,38 @@ namespace osu.Game.Tests.Visual.Navigation
         {
             AddStep("import beatmap", () =>
             {
-                var difficulty = new BeatmapDifficulty();
-                var metadata = new BeatmapMetadata
-                {
-                    Artist = "SomeArtist",
-                    AuthorString = "SomeAuthor",
-                    Title = "import"
-                };
-
                 beatmap = Game.BeatmapManager.Import(new BeatmapSetInfo
                 {
                     Hash = Guid.NewGuid().ToString(),
                     OnlineID = 1,
-                    Metadata = metadata,
                     Beatmaps =
                     {
                         new BeatmapInfo
                         {
                             OnlineID = 1 * 1024,
-                            Metadata = metadata,
-                            BaseDifficulty = difficulty,
+                            Metadata = new BeatmapMetadata
+                            {
+                                Artist = "SomeArtist",
+                                Author = { Username = "SomeAuthor" },
+                                Title = "import"
+                            },
+                            Difficulty = new BeatmapDifficulty(),
                             Ruleset = new OsuRuleset().RulesetInfo
                         },
                         new BeatmapInfo
                         {
                             OnlineID = 1 * 2048,
-                            Metadata = metadata,
-                            BaseDifficulty = difficulty,
+                            Metadata = new BeatmapMetadata
+                            {
+                                Artist = "SomeArtist",
+                                Author = { Username = "SomeAuthor" },
+                                Title = "import"
+                            },
+                            Difficulty = new BeatmapDifficulty(),
                             Ruleset = new OsuRuleset().RulesetInfo
                         },
                     }
-                }).GetResultSafely().Value;
+                }).GetResultSafely()?.Value;
             });
         }
 
@@ -131,7 +133,8 @@ namespace osu.Game.Tests.Visual.Navigation
                     Hash = Guid.NewGuid().ToString(),
                     OnlineID = i,
                     BeatmapInfo = beatmap.Beatmaps.First(),
-                    Ruleset = ruleset ?? new OsuRuleset().RulesetInfo
+                    Ruleset = ruleset ?? new OsuRuleset().RulesetInfo,
+                    User = new GuestUser(),
                 }).GetResultSafely().Value;
             });
 
diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs
index 60aabf5639..89dca77af4 100644
--- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs
+++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs
@@ -71,7 +71,7 @@ namespace osu.Game.Tests.Visual.Navigation
             PushAndConfirm(() => songSelect = new TestPlaySongSelect());
             AddUntilStep("wait for song select", () => songSelect.BeatmapSetsLoaded);
 
-            AddStep("import beatmap", () => ImportBeatmapTest.LoadQuickOszIntoOsu(Game).WaitSafely());
+            AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely());
 
             AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault);
 
@@ -105,7 +105,7 @@ namespace osu.Game.Tests.Visual.Navigation
             PushAndConfirm(() => songSelect = new TestPlaySongSelect());
             AddUntilStep("wait for song select", () => songSelect.BeatmapSetsLoaded);
 
-            AddStep("import beatmap", () => ImportBeatmapTest.LoadQuickOszIntoOsu(Game).WaitSafely());
+            AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely());
 
             AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault);
 
@@ -139,7 +139,7 @@ namespace osu.Game.Tests.Visual.Navigation
             PushAndConfirm(() => songSelect = new TestPlaySongSelect());
             AddUntilStep("wait for song select", () => songSelect.BeatmapSetsLoaded);
 
-            AddStep("import beatmap", () => ImportBeatmapTest.LoadOszIntoOsu(Game, virtualTrack: true).WaitSafely());
+            AddStep("import beatmap", () => BeatmapImportHelper.LoadOszIntoOsu(Game, virtualTrack: true).WaitSafely());
 
             AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault);
 
diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneStartupRuleset.cs b/osu.Game.Tests/Visual/Navigation/TestSceneStartupRuleset.cs
new file mode 100644
index 0000000000..85dd501fd3
--- /dev/null
+++ b/osu.Game.Tests/Visual/Navigation/TestSceneStartupRuleset.cs
@@ -0,0 +1,32 @@
+// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using NUnit.Framework;
+using osu.Framework.Development;
+using osu.Game.Configuration;
+
+namespace osu.Game.Tests.Visual.Navigation
+{
+    [TestFixture]
+    public class TestSceneStartupRuleset : OsuGameTestScene
+    {
+        protected override TestOsuGame CreateTestGame()
+        {
+            // Must be done in this function due to the RecycleLocalStorage call just before.
+            var config = DebugUtils.IsDebugBuild
+                ? new DevelopmentOsuConfigManager(LocalStorage)
+                : new OsuConfigManager(LocalStorage);
+
+            config.SetValue(OsuSetting.Ruleset, "mania");
+            config.Save();
+
+            return base.CreateTestGame();
+        }
+
+        [Test]
+        public void TestRulesetConsumed()
+        {
+            AddUntilStep("ruleset correct", () => Game.Ruleset.Value.ShortName == "mania");
+        }
+    }
+}
diff --git a/osu.Game.Tests/Visual/Online/TestSceneBeatmapDownloadButton.cs b/osu.Game.Tests/Visual/Online/TestSceneBeatmapDownloadButton.cs
index 21bf8d1c5a..d9f01622da 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneBeatmapDownloadButton.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneBeatmapDownloadButton.cs
@@ -59,7 +59,7 @@ namespace osu.Game.Tests.Visual.Online
             {
                 var beatmap = beatmaps.QueryBeatmapSet(b => b.OnlineID == 241526);
 
-                if (beatmap != null) beatmaps.Delete(beatmap);
+                if (beatmap != null) beatmaps.Delete(beatmap.Value);
             });
         }
 
diff --git a/osu.Game.Tests/Visual/Online/TestSceneScoresContainer.cs b/osu.Game.Tests/Visual/Online/TestSceneScoresContainer.cs
index be2db9a8a0..8a304110dd 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneScoresContainer.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneScoresContainer.cs
@@ -1,11 +1,15 @@
 // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
 // See the LICENCE file in the repository root for full licence text.
 
+using System;
 using System.Collections.Generic;
+using System.Linq;
+using NUnit.Framework;
 using osu.Framework.Allocation;
 using osu.Framework.Graphics;
 using osu.Framework.Graphics.Containers;
 using osu.Framework.Graphics.Shapes;
+using osu.Framework.Testing;
 using osu.Framework.Utils;
 using osu.Game.Online.API;
 using osu.Game.Online.API.Requests.Responses;
@@ -24,10 +28,11 @@ namespace osu.Game.Tests.Visual.Online
         [Cached]
         private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue);
 
-        public TestSceneScoresContainer()
-        {
-            TestScoresContainer scoresContainer;
+        private TestScoresContainer scoresContainer;
 
+        [SetUpSteps]
+        public void SetUp() => Schedule(() =>
+        {
             Child = new Container
             {
                 Anchor = Anchor.TopCentre,
@@ -41,16 +46,110 @@ namespace osu.Game.Tests.Visual.Online
                         RelativeSizeAxes = Axes.Both,
                         Colour = Color4.Black,
                     },
-                    scoresContainer = new TestScoresContainer(),
+                    scoresContainer = new TestScoresContainer
+                    {
+                        Beatmap = { Value = CreateAPIBeatmap() }
+                    }
                 }
             };
+        });
 
-            var allScores = new APIScoresCollection
+        [Test]
+        public void TestNoUserBest()
+        {
+            AddStep("Scores with no user best", () =>
+            {
+                var allScores = createScores();
+
+                allScores.UserScore = null;
+
+                scoresContainer.Scores = allScores;
+            });
+
+            AddUntilStep("wait for scores displayed", () => scoresContainer.ChildrenOfType<ScoreTableRowBackground>().Any());
+            AddAssert("no user best displayed", () => scoresContainer.ChildrenOfType<DrawableTopScore>().Count() == 1);
+
+            AddStep("Load null scores", () => scoresContainer.Scores = null);
+
+            AddUntilStep("wait for scores not displayed", () => !scoresContainer.ChildrenOfType<ScoreTableRowBackground>().Any());
+            AddAssert("no best score displayed", () => !scoresContainer.ChildrenOfType<DrawableTopScore>().Any());
+
+            AddStep("Load only one score", () =>
+            {
+                var allScores = createScores();
+
+                allScores.Scores.RemoveRange(1, allScores.Scores.Count - 1);
+
+                scoresContainer.Scores = allScores;
+            });
+
+            AddUntilStep("wait for scores not displayed", () => scoresContainer.ChildrenOfType<ScoreTableRowBackground>().Count() == 1);
+            AddAssert("no best score displayed", () => scoresContainer.ChildrenOfType<DrawableTopScore>().Count() == 1);
+        }
+
+        [Test]
+        public void TestUserBest()
+        {
+            AddStep("Load scores with personal best", () =>
+            {
+                var allScores = createScores();
+                allScores.UserScore = createUserBest();
+                scoresContainer.Scores = allScores;
+            });
+
+            AddUntilStep("wait for scores displayed", () => scoresContainer.ChildrenOfType<ScoreTableRowBackground>().Any());
+            AddAssert("best score displayed", () => scoresContainer.ChildrenOfType<DrawableTopScore>().Count() == 2);
+
+            AddStep("Load scores with personal best (null position)", () =>
+            {
+                var allScores = createScores();
+                var userBest = createUserBest();
+                userBest.Position = null;
+                allScores.UserScore = userBest;
+                scoresContainer.Scores = allScores;
+            });
+
+            AddUntilStep("wait for scores displayed", () => scoresContainer.ChildrenOfType<ScoreTableRowBackground>().Any());
+            AddAssert("best score displayed", () => scoresContainer.ChildrenOfType<DrawableTopScore>().Count() == 2);
+
+            AddStep("Load scores with personal best (first place)", () =>
+            {
+                var allScores = createScores();
+                allScores.UserScore = new APIScoreWithPosition
+                {
+                    Score = allScores.Scores.First(),
+                    Position = 1,
+                };
+                scoresContainer.Scores = allScores;
+            });
+
+            AddUntilStep("wait for scores displayed", () => scoresContainer.ChildrenOfType<ScoreTableRowBackground>().Any());
+            AddAssert("best score displayed", () => scoresContainer.ChildrenOfType<DrawableTopScore>().Count() == 1);
+
+            AddStep("Scores with no user best", () =>
+            {
+                var allScores = createScores();
+
+                allScores.UserScore = null;
+
+                scoresContainer.Scores = allScores;
+            });
+
+            AddUntilStep("best score not displayed", () => scoresContainer.ChildrenOfType<DrawableTopScore>().Count() == 1);
+        }
+
+        private int onlineID = 1;
+
+        private APIScoresCollection createScores()
+        {
+            var scores = new APIScoresCollection
             {
                 Scores = new List<APIScore>
                 {
                     new APIScore
                     {
+                        Date = DateTimeOffset.Now,
+                        OnlineID = onlineID++,
                         User = new APIUser
                         {
                             Id = 6602580,
@@ -76,6 +175,8 @@ namespace osu.Game.Tests.Visual.Online
                     },
                     new APIScore
                     {
+                        Date = DateTimeOffset.Now,
+                        OnlineID = onlineID++,
                         User = new APIUser
                         {
                             Id = 4608074,
@@ -100,6 +201,8 @@ namespace osu.Game.Tests.Visual.Online
                     },
                     new APIScore
                     {
+                        Date = DateTimeOffset.Now,
+                        OnlineID = onlineID++,
                         User = new APIUser
                         {
                             Id = 1014222,
@@ -123,6 +226,8 @@ namespace osu.Game.Tests.Visual.Online
                     },
                     new APIScore
                     {
+                        Date = DateTimeOffset.Now,
+                        OnlineID = onlineID++,
                         User = new APIUser
                         {
                             Id = 1541390,
@@ -145,6 +250,8 @@ namespace osu.Game.Tests.Visual.Online
                     },
                     new APIScore
                     {
+                        Date = DateTimeOffset.Now,
+                        OnlineID = onlineID++,
                         User = new APIUser
                         {
                             Id = 7151382,
@@ -164,85 +271,7 @@ namespace osu.Game.Tests.Visual.Online
                 }
             };
 
-            var myBestScore = new APIScoreWithPosition
-            {
-                Score = new APIScore
-                {
-                    User = new APIUser
-                    {
-                        Id = 7151382,
-                        Username = @"Mayuri Hana",
-                        Country = new Country
-                        {
-                            FullName = @"Thailand",
-                            FlagName = @"TH",
-                        },
-                    },
-                    Rank = ScoreRank.D,
-                    PP = 160,
-                    MaxCombo = 1234,
-                    TotalScore = 123456,
-                    Accuracy = 0.6543,
-                },
-                Position = 1337,
-            };
-
-            var myBestScoreWithNullPosition = new APIScoreWithPosition
-            {
-                Score = new APIScore
-                {
-                    User = new APIUser
-                    {
-                        Id = 7151382,
-                        Username = @"Mayuri Hana",
-                        Country = new Country
-                        {
-                            FullName = @"Thailand",
-                            FlagName = @"TH",
-                        },
-                    },
-                    Rank = ScoreRank.D,
-                    PP = 160,
-                    MaxCombo = 1234,
-                    TotalScore = 123456,
-                    Accuracy = 0.6543,
-                },
-                Position = null,
-            };
-
-            var oneScore = new APIScoresCollection
-            {
-                Scores = new List<APIScore>
-                {
-                    new APIScore
-                    {
-                        User = new APIUser
-                        {
-                            Id = 6602580,
-                            Username = @"waaiiru",
-                            Country = new Country
-                            {
-                                FullName = @"Spain",
-                                FlagName = @"ES",
-                            },
-                        },
-                        Mods = new[]
-                        {
-                            new APIMod { Acronym = new OsuModDoubleTime().Acronym },
-                            new APIMod { Acronym = new OsuModHidden().Acronym },
-                            new APIMod { Acronym = new OsuModFlashlight().Acronym },
-                            new APIMod { Acronym = new OsuModHardRock().Acronym },
-                        },
-                        Rank = ScoreRank.XH,
-                        PP = 200,
-                        MaxCombo = 1234,
-                        TotalScore = 1234567890,
-                        Accuracy = 1,
-                    }
-                }
-            };
-
-            foreach (var s in allScores.Scores)
+            foreach (var s in scores.Scores)
             {
                 s.Statistics = new Dictionary<string, int>
                 {
@@ -253,26 +282,34 @@ namespace osu.Game.Tests.Visual.Online
                 };
             }
 
-            AddStep("Load all scores", () =>
-            {
-                allScores.UserScore = null;
-                scoresContainer.Scores = allScores;
-            });
-            AddStep("Load null scores", () => scoresContainer.Scores = null);
-            AddStep("Load only one score", () => scoresContainer.Scores = oneScore);
-            AddStep("Load scores with my best", () =>
-            {
-                allScores.UserScore = myBestScore;
-                scoresContainer.Scores = allScores;
-            });
-
-            AddStep("Load scores with null my best position", () =>
-            {
-                allScores.UserScore = myBestScoreWithNullPosition;
-                scoresContainer.Scores = allScores;
-            });
+            return scores;
         }
 
+        private APIScoreWithPosition createUserBest() => new APIScoreWithPosition
+        {
+            Score = new APIScore
+            {
+                Date = DateTimeOffset.Now,
+                OnlineID = onlineID++,
+                User = new APIUser
+                {
+                    Id = 7151382,
+                    Username = @"Mayuri Hana",
+                    Country = new Country
+                    {
+                        FullName = @"Thailand",
+                        FlagName = @"TH",
+                    },
+                },
+                Rank = ScoreRank.D,
+                PP = 160,
+                MaxCombo = 1234,
+                TotalScore = 123456,
+                Accuracy = 0.6543,
+            },
+            Position = 1337,
+        };
+
         private class TestScoresContainer : ScoresContainer
         {
             public new APIScoresCollection Scores
diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs
index e9210496ca..11df115b1a 100644
--- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs
+++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs
@@ -44,6 +44,10 @@ namespace osu.Game.Tests.Visual.Playlists
             requestComplete = false;
             totalCount = 0;
             bindHandler();
+
+            // beatmap is required to be an actual beatmap so the scores can get their scores correctly calculated for standardised scoring.
+            // else the tests that rely on ordering will fall over.
+            Beatmap.Value = CreateWorkingBeatmap(Ruleset.Value);
         });
 
         [Test]
diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomCreation.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomCreation.cs
index e59884f4f4..bc9f759bdd 100644
--- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomCreation.cs
+++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomCreation.cs
@@ -2,6 +2,7 @@
 // See the LICENCE file in the repository root for full licence text.
 
 using System;
+using System.Diagnostics;
 using System.Linq;
 using NUnit.Framework;
 using osu.Framework.Allocation;
@@ -18,6 +19,7 @@ using osu.Game.Rulesets;
 using osu.Game.Rulesets.Osu;
 using osu.Game.Rulesets.Osu.Objects;
 using osu.Game.Screens.OnlinePlay.Components;
+using osu.Game.Screens.OnlinePlay.Match.Components;
 using osu.Game.Screens.OnlinePlay.Playlists;
 using osu.Game.Screens.Play;
 using osu.Game.Tests.Beatmaps;
@@ -33,13 +35,14 @@ namespace osu.Game.Tests.Visual.Playlists
 
         private TestPlaylistsRoomSubScreen match;
 
-        private ILive<BeatmapSetInfo> importedBeatmap;
+        private BeatmapSetInfo importedBeatmap;
 
         [BackgroundDependencyLoader]
         private void load(GameHost host, AudioManager audio)
         {
             Dependencies.Cache(rulesets = new RulesetStore(ContextFactory));
-            Dependencies.Cache(manager = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, audio, Resources, host, Beatmap.Default));
+            Dependencies.Cache(manager = new BeatmapManager(LocalStorage, ContextFactory, rulesets, API, audio, Resources, host, Beatmap.Default));
+            Dependencies.Cache(ContextFactory);
         }
 
         [SetUpSteps]
@@ -64,13 +67,15 @@ namespace osu.Game.Tests.Visual.Playlists
                 room.EndDate.Value = DateTimeOffset.Now.AddMinutes(5);
                 room.Playlist.Add(new PlaylistItem
                 {
-                    Beatmap = { Value = importedBeatmap.Value.Beatmaps.First() },
+                    Beatmap = { Value = importedBeatmap.Beatmaps.First() },
                     Ruleset = { Value = new OsuRuleset().RulesetInfo }
                 });
             });
 
             AddUntilStep("Progress details are hidden", () => match.ChildrenOfType<RoomLocalUserInfo>().FirstOrDefault()?.Parent.Alpha == 0);
 
+            AddUntilStep("Leaderboard shows two aggregate scores", () => match.ChildrenOfType<MatchLeaderboardScore>().Count(s => s.ScoreText.Text != "0") == 2);
+
             AddStep("start match", () => match.ChildrenOfType<PlaylistsReadyButton>().First().TriggerClick());
             AddUntilStep("player loader loaded", () => Stack.CurrentScreen is PlayerLoader);
         }
@@ -87,7 +92,7 @@ namespace osu.Game.Tests.Visual.Playlists
                 room.EndDate.Value = DateTimeOffset.Now.AddMinutes(5);
                 room.Playlist.Add(new PlaylistItem
                 {
-                    Beatmap = { Value = importedBeatmap.Value.Beatmaps.First() },
+                    Beatmap = { Value = importedBeatmap.Beatmaps.First() },
                     Ruleset = { Value = new OsuRuleset().RulesetInfo }
                 });
             });
@@ -104,7 +109,7 @@ namespace osu.Game.Tests.Visual.Playlists
                 room.Host.Value = API.LocalUser.Value;
                 room.Playlist.Add(new PlaylistItem
                 {
-                    Beatmap = { Value = importedBeatmap.Value.Beatmaps.First() },
+                    Beatmap = { Value = importedBeatmap.Beatmaps.First() },
                     Ruleset = { Value = new OsuRuleset().RulesetInfo }
                 });
             });
@@ -121,9 +126,9 @@ namespace osu.Game.Tests.Visual.Playlists
 
             AddStep("store real beatmap values", () =>
             {
-                realHash = importedBeatmap.Value.Beatmaps[0].MD5Hash;
-                realOnlineId = importedBeatmap.Value.Beatmaps[0].OnlineID ?? -1;
-                realOnlineSetId = importedBeatmap.Value.OnlineID ?? -1;
+                realHash = importedBeatmap.Beatmaps[0].MD5Hash;
+                realOnlineId = importedBeatmap.Beatmaps[0].OnlineID;
+                realOnlineSetId = importedBeatmap.OnlineID;
             });
 
             AddStep("import modified beatmap", () =>
@@ -133,6 +138,7 @@ namespace osu.Game.Tests.Visual.Playlists
                     BeatmapInfo =
                     {
                         OnlineID = realOnlineId,
+                        Metadata = new BeatmapMetadata(),
                         BeatmapSet =
                         {
                             OnlineID = realOnlineSetId
@@ -143,6 +149,8 @@ namespace osu.Game.Tests.Visual.Playlists
                 modifiedBeatmap.HitObjects.Clear();
                 modifiedBeatmap.HitObjects.Add(new HitCircle { StartTime = 5000 });
 
+                Debug.Assert(modifiedBeatmap.BeatmapInfo.BeatmapSet != null);
+
                 manager.Import(modifiedBeatmap.BeatmapInfo.BeatmapSet).WaitSafely();
             });
 
@@ -159,6 +167,7 @@ namespace osu.Game.Tests.Visual.Playlists
                         {
                             MD5Hash = realHash,
                             OnlineID = realOnlineId,
+                            Metadata = new BeatmapMetadata(),
                             BeatmapSet = new BeatmapSetInfo
                             {
                                 OnlineID = realOnlineSetId,
@@ -185,6 +194,8 @@ namespace osu.Game.Tests.Visual.Playlists
                     },
                 };
 
+                Debug.Assert(originalBeatmap.BeatmapInfo.BeatmapSet != null);
+
                 manager.Import(originalBeatmap.BeatmapInfo.BeatmapSet).WaitSafely();
             });
 
@@ -202,7 +213,14 @@ namespace osu.Game.Tests.Visual.Playlists
             });
         }
 
-        private void importBeatmap() => AddStep("import beatmap", () => importedBeatmap = manager.Import(CreateBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo.BeatmapSet).GetResultSafely());
+        private void importBeatmap() => AddStep("import beatmap", () =>
+        {
+            var beatmap = CreateBeatmap(new OsuRuleset().RulesetInfo);
+
+            Debug.Assert(beatmap.BeatmapInfo.BeatmapSet != null);
+
+            importedBeatmap = manager.Import(beatmap.BeatmapInfo.BeatmapSet).GetResultSafely()?.Value.Detach();
+        });
 
         private class TestPlaylistsRoomSubScreen : PlaylistsRoomSubScreen
         {
diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsScreen.cs
index e52f823f0b..63bd7c8068 100644
--- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsScreen.cs
+++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsScreen.cs
@@ -2,8 +2,6 @@
 // See the LICENCE file in the repository root for full licence text.
 
 using NUnit.Framework;
-using osu.Framework.Allocation;
-using osu.Game.Overlays;
 
 namespace osu.Game.Tests.Visual.Playlists
 {
@@ -12,9 +10,6 @@ namespace osu.Game.Tests.Visual.Playlists
     {
         protected override bool UseOnlineAPI => true;
 
-        [Cached]
-        private MusicController musicController { get; set; } = new MusicController();
-
         public TestScenePlaylistsScreen()
         {
             var multi = new Screens.OnlinePlay.Playlists.Playlists();
diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyCircle.cs b/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyCircle.cs
index 944941723e..ac736086fd 100644
--- a/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyCircle.cs
+++ b/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyCircle.cs
@@ -72,6 +72,7 @@ namespace osu.Game.Tests.Visual.Ranking
                 Username = "peppy",
             },
             BeatmapInfo = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo,
+            Ruleset = new OsuRuleset().RulesetInfo,
             Mods = new Mod[] { new OsuModHardRock(), new OsuModDoubleTime() },
             TotalScore = 2845370,
             Accuracy = accuracy,
diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs b/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs
index 2cb4fb6b6b..8b646df362 100644
--- a/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs
+++ b/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs
@@ -12,7 +12,7 @@ using osu.Framework.Graphics.Shapes;
 using osu.Framework.Testing;
 using osu.Game.Beatmaps;
 using osu.Game.Graphics.Sprites;
-using osu.Game.Online.API.Requests.Responses;
+using osu.Game.Models;
 using osu.Game.Rulesets;
 using osu.Game.Rulesets.Mods;
 using osu.Game.Rulesets.Osu;
@@ -33,7 +33,7 @@ namespace osu.Game.Tests.Visual.Ranking
         [Test]
         public void TestMapWithKnownMapper()
         {
-            var author = new APIUser { Username = "mapper_name" };
+            var author = new RealmUser { Username = "mapper_name" };
 
             AddStep("show example score", () => showPanel(TestResources.CreateTestScoreInfo(createTestBeatmap(author))));
         }
@@ -43,7 +43,7 @@ namespace osu.Game.Tests.Visual.Ranking
         {
             AddStep("show excess mods score", () =>
             {
-                var author = new APIUser { Username = "mapper_name" };
+                var author = new RealmUser { Username = "mapper_name" };
 
                 var score = TestResources.CreateTestScoreInfo(createTestBeatmap(author));
                 score.Mods = score.BeatmapInfo.Ruleset.CreateInstance().CreateAllMods().ToArray();
@@ -57,7 +57,7 @@ namespace osu.Game.Tests.Visual.Ranking
         [Test]
         public void TestMapWithUnknownMapper()
         {
-            AddStep("show example score", () => showPanel(TestResources.CreateTestScoreInfo(createTestBeatmap(new APIUser()))));
+            AddStep("show example score", () => showPanel(TestResources.CreateTestScoreInfo(createTestBeatmap(new RealmUser()))));
 
             AddAssert("mapped by text not present", () =>
                 this.ChildrenOfType<OsuSpriteText>().All(spriteText => !containsAny(spriteText.Text.ToString(), "mapped", "by")));
@@ -73,7 +73,7 @@ namespace osu.Game.Tests.Visual.Ranking
                 var ruleset = new OsuRuleset();
 
                 var mods = new Mod[] { ruleset.GetAutoplayMod() };
-                var beatmap = createTestBeatmap(new APIUser());
+                var beatmap = createTestBeatmap(new RealmUser());
 
                 var score = TestResources.CreateTestScoreInfo(beatmap);
 
@@ -89,7 +89,7 @@ namespace osu.Game.Tests.Visual.Ranking
         private void showPanel(ScoreInfo score) =>
             Child = new ExpandedPanelMiddleContentContainer(score);
 
-        private BeatmapInfo createTestBeatmap([NotNull] APIUser author)
+        private BeatmapInfo createTestBeatmap([NotNull] RealmUser author)
         {
             var beatmap = new TestBeatmap(rulesetStore.GetRuleset(0)).BeatmapInfo;
 
diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs
index 666cbf02b5..62500babc1 100644
--- a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs
+++ b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs
@@ -13,8 +13,10 @@ using osu.Framework.Screens;
 using osu.Framework.Testing;
 using osu.Framework.Utils;
 using osu.Game.Beatmaps;
+using osu.Game.Database;
 using osu.Game.Graphics.UserInterface;
 using osu.Game.Online.API;
+using osu.Game.Rulesets;
 using osu.Game.Scoring;
 using osu.Game.Screens;
 using osu.Game.Screens.Play;
@@ -23,6 +25,7 @@ using osu.Game.Screens.Ranking.Statistics;
 using osu.Game.Tests.Resources;
 using osuTK;
 using osuTK.Input;
+using Realms;
 
 namespace osu.Game.Tests.Visual.Ranking
 {
@@ -32,13 +35,22 @@ namespace osu.Game.Tests.Visual.Ranking
         [Resolved]
         private BeatmapManager beatmaps { get; set; }
 
+        [Resolved]
+        private RealmContextFactory realmContextFactory { get; set; }
+
         protected override void LoadComplete()
         {
             base.LoadComplete();
 
-            var beatmapInfo = beatmaps.QueryBeatmap(b => b.RulesetID == 0);
-            if (beatmapInfo != null)
-                Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmapInfo);
+            using (var realm = realmContextFactory.CreateContext())
+            {
+                var beatmapInfo = realm.All<BeatmapInfo>()
+                                       .Filter($"{nameof(BeatmapInfo.Ruleset)}.{nameof(RulesetInfo.OnlineID)} = $0", 0)
+                                       .FirstOrDefault();
+
+                if (beatmapInfo != null)
+                    Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmapInfo);
+            }
         }
 
         [Test]
@@ -203,7 +215,7 @@ namespace osu.Game.Tests.Visual.Ranking
         {
             DelayedFetchResultsScreen screen = null;
 
-            var tcs = new TaskCompletionSource();
+            var tcs = new TaskCompletionSource<bool>();
 
             AddStep("load results", () => Child = new TestResultsContainer(screen = new DelayedFetchResultsScreen(TestResources.CreateTestScoreInfo(), tcs.Task)));
 
@@ -218,7 +230,7 @@ namespace osu.Game.Tests.Visual.Ranking
 
             AddAssert("no fetch yet", () => !screen.FetchCompleted);
 
-            AddStep("allow fetch", () => tcs.SetResult());
+            AddStep("allow fetch", () => tcs.SetResult(true));
 
             AddUntilStep("wait for fetch", () => screen.FetchCompleted);
             AddAssert("expanded panel still on screen", () => this.ChildrenOfType<ScorePanel>().Single(p => p.State == PanelState.Expanded).ScreenSpaceDrawQuad.TopLeft.X > 0);
diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneScorePanelList.cs b/osu.Game.Tests/Visual/Ranking/TestSceneScorePanelList.cs
index f5ad352b9c..e786b85f78 100644
--- a/osu.Game.Tests/Visual/Ranking/TestSceneScorePanelList.cs
+++ b/osu.Game.Tests/Visual/Ranking/TestSceneScorePanelList.cs
@@ -7,6 +7,7 @@ using NUnit.Framework;
 using osu.Framework.Graphics;
 using osu.Framework.Testing;
 using osu.Framework.Utils;
+using osu.Game.Models;
 using osu.Game.Scoring;
 using osu.Game.Screens.Ranking;
 using osu.Game.Tests.Resources;
@@ -157,10 +158,10 @@ namespace osu.Game.Tests.Visual.Ranking
         public void TestSelectMultipleScores()
         {
             var firstScore = TestResources.CreateTestScoreInfo();
-            var secondScore = TestResources.CreateTestScoreInfo();
+            firstScore.RealmUser = new RealmUser { Username = "A" };
 
-            firstScore.UserString = "A";
-            secondScore.UserString = "B";
+            var secondScore = TestResources.CreateTestScoreInfo();
+            secondScore.RealmUser = new RealmUser { Username = "B" };
 
             createListStep(() => new ScorePanelList());
 
@@ -178,7 +179,7 @@ namespace osu.Game.Tests.Visual.Ranking
 
             AddStep("select second score", () =>
             {
-                InputManager.MoveMouseTo(list.ChildrenOfType<ScorePanel>().Single(p => p.Score == secondScore));
+                InputManager.MoveMouseTo(list.ChildrenOfType<ScorePanel>().Single(p => p.Score.Equals(secondScore)));
                 InputManager.Click(MouseButton.Left);
             });
 
@@ -303,6 +304,6 @@ namespace osu.Game.Tests.Visual.Ranking
             => AddUntilStep("first panel centred", () => Precision.AlmostEquals(list.ChildrenOfType<ScorePanel>().First().ScreenSpaceDrawQuad.Centre.X, list.ScreenSpaceDrawQuad.Centre.X, 1));
 
         private void assertScoreState(ScoreInfo score, bool expanded)
-            => AddUntilStep($"score expanded = {expanded}", () => (list.ChildrenOfType<ScorePanel>().Single(p => p.Score == score).State == PanelState.Expanded) == expanded);
+            => AddUntilStep($"score expanded = {expanded}", () => (list.ChildrenOfType<ScorePanel>().Single(p => p.Score.Equals(score)).State == PanelState.Expanded) == expanded);
     }
 }
diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneAdvancedStats.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneAdvancedStats.cs
index d57b3dec5d..7ceae0a69b 100644
--- a/osu.Game.Tests/Visual/SongSelect/TestSceneAdvancedStats.cs
+++ b/osu.Game.Tests/Visual/SongSelect/TestSceneAdvancedStats.cs
@@ -37,9 +37,8 @@ namespace osu.Game.Tests.Visual.SongSelect
 
         private BeatmapInfo exampleBeatmapInfo => new BeatmapInfo
         {
-            RulesetID = 0,
             Ruleset = rulesets.AvailableRulesets.First(),
-            BaseDifficulty = new BeatmapDifficulty
+            Difficulty = new BeatmapDifficulty
             {
                 CircleSize = 7.2f,
                 DrainRate = 3,
@@ -68,8 +67,8 @@ namespace osu.Game.Tests.Visual.SongSelect
         {
             AddStep("set beatmap", () => advancedStats.BeatmapInfo = new BeatmapInfo
             {
-                Ruleset = rulesets.GetRuleset(3),
-                BaseDifficulty = new BeatmapDifficulty
+                Ruleset = rulesets.GetRuleset(3) ?? throw new InvalidOperationException(),
+                Difficulty = new BeatmapDifficulty
                 {
                     CircleSize = 5,
                     DrainRate = 4.3f,
diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs
index f637c715a1..0298c3bea9 100644
--- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs
+++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs
@@ -6,6 +6,7 @@ using System.Collections.Generic;
 using System.Linq;
 using NUnit.Framework;
 using osu.Framework.Allocation;
+using osu.Framework.Extensions.IEnumerableExtensions;
 using osu.Framework.Graphics;
 using osu.Framework.Graphics.Containers;
 using osu.Game.Beatmaps;
@@ -27,7 +28,7 @@ namespace osu.Game.Tests.Visual.SongSelect
         private RulesetStore rulesets;
 
         private readonly Stack<BeatmapSetInfo> selectedSets = new Stack<BeatmapSetInfo>();
-        private readonly HashSet<int> eagerSelectedIDs = new HashSet<int>();
+        private readonly HashSet<Guid> eagerSelectedIDs = new HashSet<Guid>();
 
         private BeatmapInfo currentSelection => carousel.SelectedBeatmapInfo;
 
@@ -75,7 +76,7 @@ namespace osu.Game.Tests.Visual.SongSelect
                 {
                     AddStep("store selection", () => selection = carousel.SelectedBeatmapInfo);
                     if (isIterating)
-                        AddUntilStep("selection changed", () => !carousel.SelectedBeatmapInfo.Equals(selection));
+                        AddUntilStep("selection changed", () => !carousel.SelectedBeatmapInfo?.Equals(selection) == true);
                     else
                         AddUntilStep("selection not changed", () => carousel.SelectedBeatmapInfo.Equals(selection));
                 }
@@ -377,11 +378,7 @@ namespace osu.Game.Tests.Visual.SongSelect
 
                 var rulesetBeatmapSet = TestResources.CreateTestBeatmapSetInfo(1);
                 var taikoRuleset = rulesets.AvailableRulesets.ElementAt(1);
-                rulesetBeatmapSet.Beatmaps.ForEach(b =>
-                {
-                    b.Ruleset = taikoRuleset;
-                    b.RulesetID = 1;
-                });
+                rulesetBeatmapSet.Beatmaps.ForEach(b => b.Ruleset = taikoRuleset);
 
                 sets.Add(rulesetBeatmapSet);
             });
@@ -409,10 +406,10 @@ namespace osu.Game.Tests.Visual.SongSelect
                 var set = TestResources.CreateTestBeatmapSetInfo();
 
                 if (i == 4)
-                    set.Metadata.Artist = zzz_string;
+                    set.Beatmaps.ForEach(b => b.Metadata.Artist = zzz_string);
 
                 if (i == 16)
-                    set.Metadata.AuthorString = zzz_string;
+                    set.Beatmaps.ForEach(b => b.Metadata.Author.Username = zzz_string);
 
                 sets.Add(set);
             }
@@ -433,12 +430,17 @@ namespace osu.Game.Tests.Visual.SongSelect
             for (int i = 0; i < 20; i++)
             {
                 var set = TestResources.CreateTestBeatmapSetInfo();
-                set.Metadata.Artist = "same artist";
-                set.Metadata.Title = "same title";
+
+                // only need to set the first as they are a shared reference.
+                var beatmap = set.Beatmaps.First();
+
+                beatmap.Metadata.Artist = "same artist";
+                beatmap.Metadata.Title = "same title";
+
                 sets.Add(set);
             }
 
-            int idOffset = sets.First().OnlineID ?? 0;
+            int idOffset = sets.First().OnlineID;
 
             loadBeatmaps(sets);
 
@@ -577,7 +579,6 @@ namespace osu.Game.Tests.Visual.SongSelect
                 for (int i = 0; i <= 2; i++)
                 {
                     testMixed.Beatmaps[i].Ruleset = rulesets.AvailableRulesets.ElementAt(i);
-                    testMixed.Beatmaps[i].RulesetID = i;
                 }
 
                 carousel.UpdateBeatmapSet(testMixed);
@@ -599,7 +600,6 @@ namespace osu.Game.Tests.Visual.SongSelect
                 testSingle.Beatmaps.ForEach(b =>
                 {
                     b.Ruleset = rulesets.AvailableRulesets.ElementAt(1);
-                    b.RulesetID = b.Ruleset.ID ?? 1;
                 });
 
                 carousel.UpdateBeatmapSet(testSingle);
@@ -674,7 +674,7 @@ namespace osu.Game.Tests.Visual.SongSelect
                 AddStep("Restore different ruleset filter", () =>
                 {
                     carousel.Filter(new FilterCriteria { Ruleset = rulesets.GetRuleset(1) }, false);
-                    eagerSelectedIDs.Add(carousel.SelectedBeatmapSet.OnlineID ?? -1);
+                    eagerSelectedIDs.Add(carousel.SelectedBeatmapSet.ID);
                 });
 
                 AddAssert("selection changed", () => !carousel.SelectedBeatmapInfo.Equals(manySets.First().Beatmaps.First()));
@@ -697,10 +697,9 @@ namespace osu.Game.Tests.Visual.SongSelect
 
                 for (int i = 1; i <= 15; i++)
                 {
-                    set.Beatmaps.Add(new BeatmapInfo
+                    set.Beatmaps.Add(new BeatmapInfo(new OsuRuleset().RulesetInfo, new BeatmapDifficulty(), new BeatmapMetadata())
                     {
                         DifficultyName = $"Stars: {i}",
-                        Ruleset = new OsuRuleset().RulesetInfo,
                         StarRating = i,
                     });
                 }
@@ -872,8 +871,6 @@ namespace osu.Game.Tests.Visual.SongSelect
                     }
                 }
             }
-
-            protected override IEnumerable<BeatmapSetInfo> GetLoadableBeatmaps() => Enumerable.Empty<BeatmapSetInfo>();
         }
     }
 }
diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs
index 9ad5242df4..fd3f739c34 100644
--- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs
+++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs
@@ -208,7 +208,7 @@ namespace osu.Game.Tests.Visual.SongSelect
                 {
                     Metadata = new BeatmapMetadata
                     {
-                        AuthorString = $"{ruleset.ShortName}Author",
+                        Author = { Username = $"{ruleset.ShortName}Author" },
                         Artist = $"{ruleset.ShortName}Artist",
                         Source = $"{ruleset.ShortName}Source",
                         Title = $"{ruleset.ShortName}Title"
@@ -216,7 +216,7 @@ namespace osu.Game.Tests.Visual.SongSelect
                     Ruleset = ruleset,
                     StarRating = 6,
                     DifficultyName = $"{ruleset.ShortName}Version",
-                    BaseDifficulty = new BeatmapDifficulty()
+                    Difficulty = new BeatmapDifficulty()
                 },
                 HitObjects = objects
             };
@@ -230,7 +230,7 @@ namespace osu.Game.Tests.Visual.SongSelect
                 {
                     Metadata = new BeatmapMetadata
                     {
-                        AuthorString = "WWWWWWWWWWWWWWW",
+                        Author = { Username = "WWWWWWWWWWWWWWW" },
                         Artist = "Verrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrry long Artist",
                         Source = "Verrrrry long Source",
                         Title = "Verrrrry long Title"
diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs
index 605e03564d..2e1a66be5f 100644
--- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs
+++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs
@@ -16,6 +16,7 @@ using osu.Game.Online.Leaderboards;
 using osu.Game.Overlays;
 using osu.Game.Rulesets;
 using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Osu;
 using osu.Game.Rulesets.Osu.Mods;
 using osu.Game.Scoring;
 using osu.Game.Screens.Select.Leaderboards;
@@ -44,6 +45,7 @@ namespace osu.Game.Tests.Visual.SongSelect
             dependencies.Cache(rulesetStore = new RulesetStore(ContextFactory));
             dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, ContextFactory, rulesetStore, null, dependencies.Get<AudioManager>(), Resources, dependencies.Get<GameHost>(), Beatmap.Default));
             dependencies.Cache(scoreManager = new ScoreManager(rulesetStore, () => beatmapManager, LocalStorage, ContextFactory, Scheduler));
+            Dependencies.Cache(ContextFactory);
 
             return dependencies;
         }
@@ -135,6 +137,7 @@ namespace osu.Game.Tests.Visual.SongSelect
                 MaxCombo = 244,
                 TotalScore = 1707827,
                 Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock() },
+                Ruleset = new OsuRuleset().RulesetInfo,
                 User = new APIUser
                 {
                     Id = 6602580,
@@ -157,6 +160,7 @@ namespace osu.Game.Tests.Visual.SongSelect
                 Accuracy = 1,
                 MaxCombo = 244,
                 TotalScore = 1707827,
+                Ruleset = new OsuRuleset().RulesetInfo,
                 Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), },
                 User = new APIUser
                 {
@@ -182,7 +186,7 @@ namespace osu.Game.Tests.Visual.SongSelect
 
         private void clearScores()
         {
-            AddStep("Clear all scores", () => scoreManager.Delete(scoreManager.GetAllUsableScores()));
+            AddStep("Clear all scores", () => scoreManager.Delete());
         }
 
         private void checkCount(int expected) =>
@@ -199,6 +203,7 @@ namespace osu.Game.Tests.Visual.SongSelect
                     MaxCombo = 244,
                     TotalScore = 1707827,
                     //Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), },
+                    Ruleset = new OsuRuleset().RulesetInfo,
                     BeatmapInfo = beatmapInfo,
                     User = new APIUser
                     {
@@ -219,6 +224,7 @@ namespace osu.Game.Tests.Visual.SongSelect
                     TotalScore = 1707827,
                     //Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), },
                     BeatmapInfo = beatmapInfo,
+                    Ruleset = new OsuRuleset().RulesetInfo,
                     User = new APIUser
                     {
                         Id = 4608074,
@@ -238,6 +244,8 @@ namespace osu.Game.Tests.Visual.SongSelect
                     TotalScore = 1707827,
                     //Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), },
                     BeatmapInfo = beatmapInfo,
+                    Ruleset = new OsuRuleset().RulesetInfo,
+
                     User = new APIUser
                     {
                         Id = 1014222,
@@ -257,6 +265,8 @@ namespace osu.Game.Tests.Visual.SongSelect
                     TotalScore = 1707827,
                     //Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), },
                     BeatmapInfo = beatmapInfo,
+                    Ruleset = new OsuRuleset().RulesetInfo,
+
                     User = new APIUser
                     {
                         Id = 1541390,
@@ -276,6 +286,8 @@ namespace osu.Game.Tests.Visual.SongSelect
                     TotalScore = 1707827,
                     //Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), },
                     BeatmapInfo = beatmapInfo,
+                    Ruleset = new OsuRuleset().RulesetInfo,
+
                     User = new APIUser
                     {
                         Id = 2243452,
@@ -295,6 +307,8 @@ namespace osu.Game.Tests.Visual.SongSelect
                     TotalScore = 1707827,
                     //Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), },
                     BeatmapInfo = beatmapInfo,
+                    Ruleset = new OsuRuleset().RulesetInfo,
+
                     User = new APIUser
                     {
                         Id = 2705430,
@@ -314,6 +328,8 @@ namespace osu.Game.Tests.Visual.SongSelect
                     TotalScore = 1707827,
                     //Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), },
                     BeatmapInfo = beatmapInfo,
+                    Ruleset = new OsuRuleset().RulesetInfo,
+
                     User = new APIUser
                     {
                         Id = 7151382,
@@ -333,6 +349,8 @@ namespace osu.Game.Tests.Visual.SongSelect
                     TotalScore = 1707827,
                     //Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), },
                     BeatmapInfo = beatmapInfo,
+                    Ruleset = new OsuRuleset().RulesetInfo,
+
                     User = new APIUser
                     {
                         Id = 2051389,
@@ -352,6 +370,8 @@ namespace osu.Game.Tests.Visual.SongSelect
                     TotalScore = 1707827,
                     //Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), },
                     BeatmapInfo = beatmapInfo,
+                    Ruleset = new OsuRuleset().RulesetInfo,
+
                     User = new APIUser
                     {
                         Id = 6169483,
@@ -371,6 +391,8 @@ namespace osu.Game.Tests.Visual.SongSelect
                     TotalScore = 1707827,
                     //Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), },
                     BeatmapInfo = beatmapInfo,
+                    Ruleset = new OsuRuleset().RulesetInfo,
+
                     User = new APIUser
                     {
                         Id = 6702666,
diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapMetadataDisplay.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapMetadataDisplay.cs
index e573c96ce9..fb6d9a0b4b 100644
--- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapMetadataDisplay.cs
+++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapMetadataDisplay.cs
@@ -77,7 +77,7 @@ namespace osu.Game.Tests.Visual.SongSelect
         {
             showMetadataForBeatmap(() =>
             {
-                var allBeatmapSets = manager.GetAllUsableBeatmapSets(IncludedDetails.Minimal);
+                var allBeatmapSets = manager.GetAllUsableBeatmapSets();
                 if (allBeatmapSets.Count == 0)
                     return manager.DefaultBeatmap;
 
diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs
index 08b5802713..b7bc0c37e1 100644
--- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs
+++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs
@@ -113,6 +113,8 @@ namespace osu.Game.Tests.Visual.SongSelect
             // Switch to catch
             presentAndConfirm(() => catchSet, 1);
 
+            AddAssert("game-wide ruleset changed", () => Game.Ruleset.Value.Equals(catchSet.Beatmaps.First().Ruleset));
+
             // Present mixed difficulty set, expect current ruleset to be selected
             presentAndConfirm(() => mixedSet, 2);
         }
@@ -182,7 +184,7 @@ namespace osu.Game.Tests.Visual.SongSelect
                 beatmap.DifficultyName = $"SR{i + 1}";
             }
 
-            return Game.BeatmapManager.Import(beatmapSet).GetResultSafely().Value;
+            return Game.BeatmapManager.Import(beatmapSet).GetResultSafely()?.Value;
         }
 
         private bool ensureAllBeatmapSetsImported(IEnumerable<BeatmapSetInfo> beatmapSets) => beatmapSets.All(set => set != null);
diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs
index 1ee59eccc7..ca8e9d2eff 100644
--- a/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs
+++ b/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs
@@ -38,6 +38,7 @@ namespace osu.Game.Tests.Visual.SongSelect
         {
             Dependencies.Cache(rulesets = new RulesetStore(ContextFactory));
             Dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, Audio, Resources, host, Beatmap.Default));
+            Dependencies.Cache(ContextFactory);
 
             beatmapManager.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely();
 
diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs
index 37f110e727..6295a52bdd 100644
--- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs
+++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs
@@ -45,7 +45,10 @@ namespace osu.Game.Tests.Visual.SongSelect
         [BackgroundDependencyLoader]
         private void load(GameHost host, AudioManager audio)
         {
+            // These DI caches are required to ensure for interactive runs this test scene doesn't nuke all user beatmaps in the local install.
+            // At a point we have isolated interactive test runs enough, this can likely be removed.
             Dependencies.Cache(rulesets = new RulesetStore(ContextFactory));
+            Dependencies.Cache(ContextFactory);
             Dependencies.Cache(manager = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, audio, Resources, host, defaultBeatmap = Beatmap.Default));
 
             Dependencies.Cache(music = new MusicController());
@@ -585,7 +588,7 @@ namespace osu.Game.Tests.Visual.SongSelect
         [Test]
         public void TestHideSetSelectsCorrectBeatmap()
         {
-            int? previousID = null;
+            Guid? previousID = null;
             createSongSelect();
             addRulesetImportStep(0);
             AddStep("Move to last difficulty", () => songSelect.Carousel.SelectBeatmap(songSelect.Carousel.BeatmapSets.First().Beatmaps.Last()));
@@ -637,8 +640,9 @@ namespace osu.Game.Tests.Visual.SongSelect
 
             AddStep("Get filtered icon", () =>
             {
-                filteredBeatmap = songSelect.Carousel.SelectedBeatmapSet.Beatmaps.First(b => b.BPM < maxBPM);
-                int filteredBeatmapIndex = getBeatmapIndex(filteredBeatmap.BeatmapSet, filteredBeatmap);
+                var selectedSet = songSelect.Carousel.SelectedBeatmapSet;
+                filteredBeatmap = selectedSet.Beatmaps.First(b => b.BPM < maxBPM);
+                int filteredBeatmapIndex = getBeatmapIndex(selectedSet, filteredBeatmap);
                 filteredIcon = set.ChildrenOfType<FilterableDifficultyIcon>().ElementAt(filteredBeatmapIndex);
             });
 
@@ -744,7 +748,7 @@ namespace osu.Game.Tests.Visual.SongSelect
 
             AddUntilStep("Check ruleset changed to mania", () => Ruleset.Value.OnlineID == 3);
 
-            AddAssert("Selected beatmap still same set", () => songSelect.Carousel.SelectedBeatmapInfo.BeatmapSet.OnlineID == previousSetID);
+            AddAssert("Selected beatmap still same set", () => songSelect.Carousel.SelectedBeatmapInfo.BeatmapSet?.OnlineID == previousSetID);
             AddAssert("Selected beatmap is mania", () => Beatmap.Value.BeatmapInfo.Ruleset.OnlineID == 3);
         }
 
@@ -760,7 +764,7 @@ namespace osu.Game.Tests.Visual.SongSelect
             AddStep("import huge difficulty count map", () =>
             {
                 var usableRulesets = rulesets.AvailableRulesets.Where(r => r.OnlineID != 2).ToArray();
-                imported = manager.Import(TestResources.CreateTestBeatmapSetInfo(50, usableRulesets)).GetResultSafely().Value;
+                imported = manager.Import(TestResources.CreateTestBeatmapSetInfo(50, usableRulesets)).GetResultSafely()?.Value;
             });
 
             AddStep("select the first beatmap of import", () => Beatmap.Value = manager.GetWorkingBeatmap(imported.Beatmaps.First()));
@@ -796,8 +800,8 @@ namespace osu.Game.Tests.Visual.SongSelect
         [Test]
         public void TestChangeRulesetWhilePresentingScore()
         {
-            BeatmapInfo getPresentBeatmap() => manager.QueryBeatmap(b => !b.BeatmapSet.DeletePending && b.RulesetID == 0);
-            BeatmapInfo getSwitchBeatmap() => manager.QueryBeatmap(b => !b.BeatmapSet.DeletePending && b.RulesetID == 1);
+            BeatmapInfo getPresentBeatmap() => manager.GetAllUsableBeatmapSets().Where(s => !s.DeletePending).SelectMany(s => s.Beatmaps).First(b => b.RulesetID == 0);
+            BeatmapInfo getSwitchBeatmap() => manager.GetAllUsableBeatmapSets().Where(s => !s.DeletePending).SelectMany(s => s.Beatmaps).First(b => b.RulesetID == 1);
 
             changeRuleset(0);
 
@@ -828,8 +832,8 @@ namespace osu.Game.Tests.Visual.SongSelect
         [Test]
         public void TestChangeBeatmapWhilePresentingScore()
         {
-            BeatmapInfo getPresentBeatmap() => manager.QueryBeatmap(b => !b.BeatmapSet.DeletePending && b.RulesetID == 0);
-            BeatmapInfo getSwitchBeatmap() => manager.QueryBeatmap(b => !b.BeatmapSet.DeletePending && b.RulesetID == 1);
+            BeatmapInfo getPresentBeatmap() => manager.GetAllUsableBeatmapSets().Where(s => !s.DeletePending).SelectMany(s => s.Beatmaps).First(b => b.RulesetID == 0);
+            BeatmapInfo getSwitchBeatmap() => manager.GetAllUsableBeatmapSets().Where(s => !s.DeletePending).SelectMany(s => s.Beatmaps).First(b => b.RulesetID == 1);
 
             changeRuleset(0);
 
diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneTopLocalRank.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneTopLocalRank.cs
new file mode 100644
index 0000000000..3aa5a759e6
--- /dev/null
+++ b/osu.Game.Tests/Visual/SongSelect/TestSceneTopLocalRank.cs
@@ -0,0 +1,143 @@
+// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Linq;
+using NUnit.Framework;
+using osu.Framework.Allocation;
+using osu.Framework.Audio;
+using osu.Framework.Extensions;
+using osu.Framework.Graphics;
+using osu.Framework.Platform;
+using osu.Framework.Testing;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets;
+using osu.Game.Scoring;
+using osu.Game.Screens.Select.Carousel;
+using osu.Game.Tests.Resources;
+using osuTK;
+
+namespace osu.Game.Tests.Visual.SongSelect
+{
+    public class TestSceneTopLocalRank : OsuTestScene
+    {
+        private RulesetStore rulesets;
+        private BeatmapManager beatmapManager;
+        private ScoreManager scoreManager;
+        private TopLocalRank topLocalRank;
+
+        [BackgroundDependencyLoader]
+        private void load(GameHost host, AudioManager audio)
+        {
+            Dependencies.Cache(rulesets = new RulesetStore(ContextFactory));
+            Dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, audio, Resources, host, Beatmap.Default));
+            Dependencies.Cache(scoreManager = new ScoreManager(rulesets, () => beatmapManager, LocalStorage, ContextFactory, Scheduler));
+            Dependencies.Cache(ContextFactory);
+
+            beatmapManager.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely();
+        }
+
+        private BeatmapInfo importedBeatmap => beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps.First(b => b.Ruleset.ShortName == "osu");
+
+        [SetUpSteps]
+        public void SetUpSteps()
+        {
+            AddStep("Delete all scores", () => scoreManager.Delete());
+
+            AddStep("Create local rank", () =>
+            {
+                Add(topLocalRank = new TopLocalRank(importedBeatmap)
+                {
+                    Anchor = Anchor.Centre,
+                    Origin = Anchor.Centre,
+                    Scale = new Vector2(10),
+                });
+            });
+        }
+
+        [Test]
+        public void TestBasicImportDelete()
+        {
+            ScoreInfo testScoreInfo = null;
+
+            AddAssert("Initially not present", () => !topLocalRank.IsPresent);
+
+            AddStep("Add score for current user", () =>
+            {
+                testScoreInfo = TestResources.CreateTestScoreInfo(importedBeatmap);
+
+                testScoreInfo.User = API.LocalUser.Value;
+                testScoreInfo.Rank = ScoreRank.B;
+
+                scoreManager.Import(testScoreInfo);
+            });
+
+            AddUntilStep("Became present", () => topLocalRank.IsPresent);
+            AddAssert("Correct rank", () => topLocalRank.Rank == ScoreRank.B);
+
+            AddStep("Delete score", () =>
+            {
+                scoreManager.Delete(testScoreInfo);
+            });
+
+            AddUntilStep("Became not present", () => !topLocalRank.IsPresent);
+        }
+
+        [Test]
+        public void TestRulesetChange()
+        {
+            ScoreInfo testScoreInfo;
+
+            AddStep("Add score for current user", () =>
+            {
+                testScoreInfo = TestResources.CreateTestScoreInfo(importedBeatmap);
+
+                testScoreInfo.User = API.LocalUser.Value;
+                testScoreInfo.Rank = ScoreRank.B;
+
+                scoreManager.Import(testScoreInfo);
+            });
+
+            AddUntilStep("Wait for initial presence", () => topLocalRank.IsPresent);
+
+            AddStep("Change ruleset", () => Ruleset.Value = rulesets.GetRuleset("fruits"));
+            AddUntilStep("Became not present", () => !topLocalRank.IsPresent);
+
+            AddStep("Change ruleset back", () => Ruleset.Value = rulesets.GetRuleset("osu"));
+            AddUntilStep("Became present", () => topLocalRank.IsPresent);
+        }
+
+        [Test]
+        public void TestHigherScoreSet()
+        {
+            ScoreInfo testScoreInfo = null;
+
+            AddAssert("Initially not present", () => !topLocalRank.IsPresent);
+
+            AddStep("Add score for current user", () =>
+            {
+                testScoreInfo = TestResources.CreateTestScoreInfo(importedBeatmap);
+
+                testScoreInfo.User = API.LocalUser.Value;
+                testScoreInfo.Rank = ScoreRank.B;
+
+                scoreManager.Import(testScoreInfo);
+            });
+
+            AddUntilStep("Became present", () => topLocalRank.IsPresent);
+            AddAssert("Correct rank", () => topLocalRank.Rank == ScoreRank.B);
+
+            AddStep("Add higher score for current user", () =>
+            {
+                var testScoreInfo2 = TestResources.CreateTestScoreInfo(importedBeatmap);
+
+                testScoreInfo2.User = API.LocalUser.Value;
+                testScoreInfo2.Rank = ScoreRank.S;
+                testScoreInfo2.TotalScore = testScoreInfo.TotalScore + 1;
+
+                scoreManager.Import(testScoreInfo2);
+            });
+
+            AddAssert("Correct rank", () => topLocalRank.Rank == ScoreRank.S);
+        }
+    }
+}
diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneUserTopScoreContainer.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneUserTopScoreContainer.cs
index 7af9e9eb40..dd7f9951bf 100644
--- a/osu.Game.Tests/Visual/SongSelect/TestSceneUserTopScoreContainer.cs
+++ b/osu.Game.Tests/Visual/SongSelect/TestSceneUserTopScoreContainer.cs
@@ -10,6 +10,7 @@ using osuTK.Graphics;
 using osu.Game.Online.Leaderboards;
 using osu.Game.Overlays;
 using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Osu;
 using osu.Game.Scoring;
 using osu.Game.Rulesets.Osu.Mods;
 using osu.Game.Users;
@@ -61,6 +62,7 @@ namespace osu.Game.Tests.Visual.SongSelect
                     MaxCombo = 244,
                     TotalScore = 1707827,
                     Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), },
+                    Ruleset = new OsuRuleset().RulesetInfo,
                     User = new APIUser
                     {
                         Id = 6602580,
@@ -79,6 +81,7 @@ namespace osu.Game.Tests.Visual.SongSelect
                     Accuracy = 1,
                     MaxCombo = 244,
                     TotalScore = 1707827,
+                    Ruleset = new OsuRuleset().RulesetInfo,
                     User = new APIUser
                     {
                         Id = 4608074,
@@ -97,6 +100,7 @@ namespace osu.Game.Tests.Visual.SongSelect
                     Accuracy = 1,
                     MaxCombo = 244,
                     TotalScore = 1707827,
+                    Ruleset = new OsuRuleset().RulesetInfo,
                     User = new APIUser
                     {
                         Id = 1541390,
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs
index a436fc0bfa..1e14e4b3e5 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs
@@ -1,13 +1,14 @@
 // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
 // See the LICENCE file in the repository root for full licence text.
 
+using System;
 using System.Collections.Generic;
 using System.Linq;
 using NUnit.Framework;
-using osu.Framework.Graphics;
 using osu.Framework.Allocation;
 using osu.Framework.Audio;
 using osu.Framework.Extensions;
+using osu.Framework.Graphics;
 using osu.Framework.Graphics.Cursor;
 using osu.Framework.Platform;
 using osu.Framework.Testing;
@@ -16,10 +17,12 @@ using osu.Game.Beatmaps;
 using osu.Game.Database;
 using osu.Game.Graphics.Cursor;
 using osu.Game.Graphics.UserInterface;
+using osu.Game.Models;
 using osu.Game.Online.API.Requests.Responses;
 using osu.Game.Online.Leaderboards;
 using osu.Game.Overlays;
 using osu.Game.Rulesets;
+using osu.Game.Rulesets.Osu;
 using osu.Game.Scoring;
 using osu.Game.Screens.Select.Leaderboards;
 using osu.Game.Tests.Resources;
@@ -41,6 +44,9 @@ namespace osu.Game.Tests.Visual.UserInterface
 
         private BeatmapInfo beatmapInfo;
 
+        [Resolved]
+        private RealmContextFactory realmFactory { get; set; }
+
         [Cached]
         private readonly DialogOverlay dialogOverlay;
 
@@ -59,13 +65,12 @@ namespace osu.Game.Tests.Visual.UserInterface
                         Scope = BeatmapLeaderboardScope.Local,
                         BeatmapInfo = new BeatmapInfo
                         {
-                            ID = 1,
+                            ID = Guid.NewGuid(),
                             Metadata = new BeatmapMetadata
                             {
-                                ID = 1,
                                 Title = "TestSong",
                                 Artist = "TestArtist",
-                                Author = new APIUser
+                                Author = new RealmUser
                                 {
                                     Username = "TestAuthor"
                                 },
@@ -84,26 +89,32 @@ namespace osu.Game.Tests.Visual.UserInterface
 
             dependencies.Cache(rulesetStore = new RulesetStore(ContextFactory));
             dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, ContextFactory, rulesetStore, null, dependencies.Get<AudioManager>(), Resources, dependencies.Get<GameHost>(), Beatmap.Default));
-            dependencies.Cache(scoreManager = new ScoreManager(rulesetStore, () => beatmapManager, LocalStorage, ContextFactory, Scheduler));
+            dependencies.Cache(scoreManager = new ScoreManager(dependencies.Get<RulesetStore>(), () => beatmapManager, LocalStorage, ContextFactory, Scheduler));
+            Dependencies.Cache(ContextFactory);
 
-            beatmapInfo = beatmapManager.Import(new ImportTask(TestResources.GetQuickTestBeatmapForImport())).GetResultSafely().Value.Beatmaps[0];
+            var imported = beatmapManager.Import(new ImportTask(TestResources.GetQuickTestBeatmapForImport())).GetResultSafely();
 
-            for (int i = 0; i < 50; i++)
+            imported?.PerformRead(s =>
             {
-                var score = new ScoreInfo
-                {
-                    OnlineID = i,
-                    BeatmapInfo = beatmapInfo,
-                    BeatmapInfoID = beatmapInfo.ID,
-                    Accuracy = RNG.NextDouble(),
-                    TotalScore = RNG.Next(1, 1000000),
-                    MaxCombo = RNG.Next(1, 1000),
-                    Rank = ScoreRank.XH,
-                    User = new APIUser { Username = "TestUser" },
-                };
+                beatmapInfo = s.Beatmaps[0];
 
-                importedScores.Add(scoreManager.Import(score).GetResultSafely().Value);
-            }
+                for (int i = 0; i < 50; i++)
+                {
+                    var score = new ScoreInfo
+                    {
+                        OnlineID = i,
+                        BeatmapInfo = beatmapInfo,
+                        Accuracy = RNG.NextDouble(),
+                        TotalScore = RNG.Next(1, 1000000),
+                        MaxCombo = RNG.Next(1, 1000),
+                        Rank = ScoreRank.XH,
+                        User = new APIUser { Username = "TestUser" },
+                        Ruleset = new OsuRuleset().RulesetInfo,
+                    };
+
+                    importedScores.Add(scoreManager.Import(score).GetResultSafely().Value);
+                }
+            });
 
             return dependencies;
         }
@@ -111,8 +122,11 @@ namespace osu.Game.Tests.Visual.UserInterface
         [SetUp]
         public void Setup() => Schedule(() =>
         {
-            // Due to soft deletions, we can re-use deleted scores between test runs
-            scoreManager.Undelete(scoreManager.QueryScores(s => s.DeletePending).ToList());
+            using (var realm = realmFactory.CreateContext())
+            {
+                // Due to soft deletions, we can re-use deleted scores between test runs
+                scoreManager.Undelete(realm.All<ScoreInfo>().Where(s => s.DeletePending).ToList());
+            }
 
             leaderboard.Scores = null;
             leaderboard.FinishTransforms(true); // After setting scores, we may be waiting for transforms to expire drawables
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModDifficultyAdjustSettings.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModDifficultyAdjustSettings.cs
index f8652573f4..8e1f426f7b 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneModDifficultyAdjustSettings.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModDifficultyAdjustSettings.cs
@@ -267,7 +267,7 @@ namespace osu.Game.Tests.Visual.UserInterface
             {
                 BeatmapInfo = new BeatmapInfo
                 {
-                    BaseDifficulty = new BeatmapDifficulty
+                    Difficulty = new BeatmapDifficulty
                     {
                         OverallDifficulty = value,
                         CircleSize = value,
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs
index f196bbd76e..b429619044 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs
@@ -459,6 +459,8 @@ namespace osu.Game.Tests.Visual.UserInterface
 
         private class TestUnimplementedModOsuRuleset : OsuRuleset
         {
+            public override string ShortName => "unimplemented";
+
             public override IEnumerable<Mod> GetModsFor(ModType type)
             {
                 if (type == ModType.Conversion) return base.GetModsFor(type).Concat(new[] { new TestUnimplementedMod() });
diff --git a/osu.Game.Tests/Visual/UserInterface/TestScenePlaylistOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestScenePlaylistOverlay.cs
index 39146d584c..62f3b63780 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestScenePlaylistOverlay.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestScenePlaylistOverlay.cs
@@ -71,7 +71,7 @@ namespace osu.Game.Tests.Visual.UserInterface
                 InputManager.MoveMouseTo(item.ScreenSpaceDrawQuad.Centre);
             });
 
-            AddAssert("song 1 is 5th", () => beatmapSets[4] == first);
+            AddAssert("song 1 is 5th", () => beatmapSets[4].Equals(first));
 
             AddStep("release handle", () => InputManager.ReleaseButton(MouseButton.Left));
         }
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneUpdateableBeatmapBackgroundSprite.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneUpdateableBeatmapBackgroundSprite.cs
index 6fe1ccc037..7109a55e7e 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneUpdateableBeatmapBackgroundSprite.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneUpdateableBeatmapBackgroundSprite.cs
@@ -14,7 +14,6 @@ using osu.Game.Graphics.Containers;
 using osu.Game.Online.API;
 using osu.Game.Online.API.Requests;
 using osu.Game.Online.API.Requests.Responses;
-using osu.Game.Rulesets;
 using osu.Game.Tests.Beatmaps.IO;
 using osuTK;
 
@@ -28,11 +27,11 @@ namespace osu.Game.Tests.Visual.UserInterface
         private IAPIProvider api;
 
         [BackgroundDependencyLoader]
-        private void load(OsuGameBase osu, IAPIProvider api, RulesetStore rulesets)
+        private void load(OsuGameBase osu, IAPIProvider api)
         {
             this.api = api;
 
-            testBeatmap = ImportBeatmapTest.LoadOszIntoOsu(osu).GetResultSafely();
+            testBeatmap = BeatmapImportHelper.LoadOszIntoOsu(osu).GetResultSafely();
         }
 
         [Test]
diff --git a/osu.Game.Tournament.Tests/NonVisual/DataLoadTest.cs b/osu.Game.Tournament.Tests/NonVisual/DataLoadTest.cs
index db019f9242..65753bfe00 100644
--- a/osu.Game.Tournament.Tests/NonVisual/DataLoadTest.cs
+++ b/osu.Game.Tournament.Tests/NonVisual/DataLoadTest.cs
@@ -35,9 +35,9 @@ namespace osu.Game.Tournament.Tests.NonVisual
 
         public class TestTournament : TournamentGameBase
         {
-            [BackgroundDependencyLoader]
-            private void load()
+            protected override void LoadComplete()
             {
+                base.LoadComplete();
                 Ruleset.Value = new RulesetInfo(); // not available
             }
         }
diff --git a/osu.Game.Tournament.Tests/TestSceneTournamentSceneManager.cs b/osu.Game.Tournament.Tests/TestSceneTournamentSceneManager.cs
index 4d134ce4af..53591da07b 100644
--- a/osu.Game.Tournament.Tests/TestSceneTournamentSceneManager.cs
+++ b/osu.Game.Tournament.Tests/TestSceneTournamentSceneManager.cs
@@ -2,14 +2,13 @@
 // See the LICENCE file in the repository root for full licence text.
 
 using osu.Framework.Allocation;
-using osu.Framework.Platform;
 
 namespace osu.Game.Tournament.Tests
 {
     public class TestSceneTournamentSceneManager : TournamentTestScene
     {
         [BackgroundDependencyLoader]
-        private void load(Storage storage)
+        private void load()
         {
             Add(new TournamentSceneManager());
         }
diff --git a/osu.Game.Tournament/Components/DrawableTeamTitle.cs b/osu.Game.Tournament/Components/DrawableTeamTitle.cs
index 5aac37259f..6732eb152f 100644
--- a/osu.Game.Tournament/Components/DrawableTeamTitle.cs
+++ b/osu.Game.Tournament/Components/DrawableTeamTitle.cs
@@ -4,7 +4,6 @@
 using JetBrains.Annotations;
 using osu.Framework.Allocation;
 using osu.Framework.Bindables;
-using osu.Framework.Graphics.Textures;
 using osu.Game.Tournament.Models;
 
 namespace osu.Game.Tournament.Components
@@ -22,7 +21,7 @@ namespace osu.Game.Tournament.Components
         }
 
         [BackgroundDependencyLoader]
-        private void load(TextureStore textures)
+        private void load()
         {
             if (team == null) return;
 
diff --git a/osu.Game.Tournament/Components/DrawableTournamentTeam.cs b/osu.Game.Tournament/Components/DrawableTournamentTeam.cs
index b9442a67f5..367e447947 100644
--- a/osu.Game.Tournament/Components/DrawableTournamentTeam.cs
+++ b/osu.Game.Tournament/Components/DrawableTournamentTeam.cs
@@ -5,7 +5,6 @@ using JetBrains.Annotations;
 using osu.Framework.Allocation;
 using osu.Framework.Bindables;
 using osu.Framework.Graphics.Containers;
-using osu.Framework.Graphics.Textures;
 using osu.Game.Graphics;
 using osu.Game.Tournament.Models;
 
@@ -33,7 +32,7 @@ namespace osu.Game.Tournament.Components
         }
 
         [BackgroundDependencyLoader]
-        private void load(TextureStore textures)
+        private void load()
         {
             if (Team == null) return;
 
diff --git a/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs b/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs
index 364cccd076..4189f3ccb5 100644
--- a/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs
+++ b/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs
@@ -9,7 +9,6 @@ using osu.Framework.Bindables;
 using osu.Framework.Graphics;
 using osu.Framework.Graphics.Containers;
 using osu.Framework.Graphics.Shapes;
-using osu.Framework.Graphics.Textures;
 using osu.Game.Beatmaps;
 using osu.Game.Beatmaps.Drawables;
 using osu.Game.Graphics;
@@ -45,7 +44,7 @@ namespace osu.Game.Tournament.Components
         }
 
         [BackgroundDependencyLoader]
-        private void load(LadderInfo ladder, TextureStore textures)
+        private void load(LadderInfo ladder)
         {
             currentMatch.BindValueChanged(matchChanged);
             currentMatch.BindTo(ladder.CurrentMatch);
diff --git a/osu.Game.Tournament/Screens/Editors/RoundEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/RoundEditorScreen.cs
index f6bc607447..5c12d83d1c 100644
--- a/osu.Game.Tournament/Screens/Editors/RoundEditorScreen.cs
+++ b/osu.Game.Tournament/Screens/Editors/RoundEditorScreen.cs
@@ -12,7 +12,6 @@ using osu.Game.Online.API;
 using osu.Game.Online.API.Requests;
 using osu.Game.Online.API.Requests.Responses;
 using osu.Game.Overlays.Settings;
-using osu.Game.Rulesets;
 using osu.Game.Tournament.Components;
 using osu.Game.Tournament.Models;
 using osuTK;
@@ -218,7 +217,7 @@ namespace osu.Game.Tournament.Screens.Editors
                     }
 
                     [BackgroundDependencyLoader]
-                    private void load(RulesetStore rulesets)
+                    private void load()
                     {
                         beatmapId.Value = Model.ID;
                         beatmapId.BindValueChanged(id =>
diff --git a/osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs
index 5d2fddffd9..5cdfe7dc08 100644
--- a/osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs
+++ b/osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs
@@ -12,7 +12,6 @@ using osu.Game.Online.API;
 using osu.Game.Online.API.Requests;
 using osu.Game.Online.API.Requests.Responses;
 using osu.Game.Overlays.Settings;
-using osu.Game.Rulesets;
 using osu.Game.Tournament.Components;
 using osu.Game.Tournament.Models;
 using osuTK;
@@ -220,7 +219,7 @@ namespace osu.Game.Tournament.Screens.Editors
                     }
 
                     [BackgroundDependencyLoader]
-                    private void load(RulesetStore rulesets)
+                    private void load()
                     {
                         beatmapId.Value = Model.ID;
                         beatmapId.BindValueChanged(id =>
diff --git a/osu.Game.Tournament/Screens/Gameplay/Components/TournamentMatchScoreDisplay.cs b/osu.Game.Tournament/Screens/Gameplay/Components/TournamentMatchScoreDisplay.cs
index 813bed86ae..db15a46fc8 100644
--- a/osu.Game.Tournament/Screens/Gameplay/Components/TournamentMatchScoreDisplay.cs
+++ b/osu.Game.Tournament/Screens/Gameplay/Components/TournamentMatchScoreDisplay.cs
@@ -11,7 +11,6 @@ using osu.Game.Graphics;
 using osu.Game.Graphics.Sprites;
 using osu.Game.Graphics.UserInterface;
 using osu.Game.Tournament.IPC;
-using osu.Game.Tournament.Models;
 using osuTK;
 
 namespace osu.Game.Tournament.Screens.Gameplay.Components
@@ -91,7 +90,7 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components
         }
 
         [BackgroundDependencyLoader]
-        private void load(LadderInfo ladder, MatchIPCInfo ipc)
+        private void load(MatchIPCInfo ipc)
         {
             score1.BindValueChanged(_ => updateScores());
             score1.BindTo(ipc.Score1);
diff --git a/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs b/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs
index 7e7c719152..f900dd7eac 100644
--- a/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs
+++ b/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs
@@ -6,7 +6,6 @@ using osu.Framework.Bindables;
 using osu.Framework.Graphics;
 using osu.Framework.Graphics.Containers;
 using osu.Framework.Graphics.Shapes;
-using osu.Framework.Platform;
 using osu.Framework.Threading;
 using osu.Game.Graphics.UserInterface;
 using osu.Game.Overlays.Settings;
@@ -37,7 +36,7 @@ namespace osu.Game.Tournament.Screens.Gameplay
         private Drawable chroma;
 
         [BackgroundDependencyLoader]
-        private void load(LadderInfo ladder, MatchIPCInfo ipc, Storage storage)
+        private void load(LadderInfo ladder, MatchIPCInfo ipc)
         {
             this.ipc = ipc;
 
diff --git a/osu.Game.Tournament/Screens/Ladder/Components/DrawableMatchTeam.cs b/osu.Game.Tournament/Screens/Ladder/Components/DrawableMatchTeam.cs
index bb1e4d2eff..ea453a53ca 100644
--- a/osu.Game.Tournament/Screens/Ladder/Components/DrawableMatchTeam.cs
+++ b/osu.Game.Tournament/Screens/Ladder/Components/DrawableMatchTeam.cs
@@ -81,7 +81,7 @@ namespace osu.Game.Tournament.Screens.Ladder.Components
         }
 
         [BackgroundDependencyLoader(true)]
-        private void load(OsuColour colours, LadderEditorScreen ladderEditor)
+        private void load(LadderEditorScreen ladderEditor)
         {
             this.ladderEditor = ladderEditor;
 
diff --git a/osu.Game.Tournament/Screens/Ladder/LadderScreen.cs b/osu.Game.Tournament/Screens/Ladder/LadderScreen.cs
index 534c402f6c..ad6e304c80 100644
--- a/osu.Game.Tournament/Screens/Ladder/LadderScreen.cs
+++ b/osu.Game.Tournament/Screens/Ladder/LadderScreen.cs
@@ -9,8 +9,6 @@ using osu.Framework.Extensions.Color4Extensions;
 using osu.Framework.Graphics;
 using osu.Framework.Graphics.Containers;
 using osu.Framework.Graphics.Lines;
-using osu.Framework.Platform;
-using osu.Game.Graphics;
 using osu.Game.Tournament.Components;
 using osu.Game.Tournament.Models;
 using osu.Game.Tournament.Screens.Editors;
@@ -30,7 +28,7 @@ namespace osu.Game.Tournament.Screens.Ladder
         protected Container Content;
 
         [BackgroundDependencyLoader]
-        private void load(OsuColour colours, Storage storage)
+        private void load()
         {
             normalPathColour = Color4Extensions.FromHex("#66D1FF");
             losersPathColour = Color4Extensions.FromHex("#FFC700");
diff --git a/osu.Game.Tournament/Screens/Schedule/ScheduleScreen.cs b/osu.Game.Tournament/Screens/Schedule/ScheduleScreen.cs
index e08be65465..84f38170ea 100644
--- a/osu.Game.Tournament/Screens/Schedule/ScheduleScreen.cs
+++ b/osu.Game.Tournament/Screens/Schedule/ScheduleScreen.cs
@@ -8,7 +8,6 @@ using osu.Framework.Bindables;
 using osu.Framework.Graphics;
 using osu.Framework.Graphics.Containers;
 using osu.Framework.Graphics.Shapes;
-using osu.Framework.Platform;
 using osu.Game.Graphics;
 using osu.Game.Tournament.Components;
 using osu.Game.Tournament.Models;
@@ -25,7 +24,7 @@ namespace osu.Game.Tournament.Screens.Schedule
         private LadderInfo ladder;
 
         [BackgroundDependencyLoader]
-        private void load(LadderInfo ladder, Storage storage)
+        private void load(LadderInfo ladder)
         {
             this.ladder = ladder;
 
diff --git a/osu.Game.Tournament/Screens/TeamIntro/SeedingScreen.cs b/osu.Game.Tournament/Screens/TeamIntro/SeedingScreen.cs
index cd74a75b10..0003e213e7 100644
--- a/osu.Game.Tournament/Screens/TeamIntro/SeedingScreen.cs
+++ b/osu.Game.Tournament/Screens/TeamIntro/SeedingScreen.cs
@@ -8,7 +8,6 @@ using osu.Framework.Graphics.Containers;
 using osu.Framework.Graphics.Shapes;
 using osu.Framework.Graphics.Sprites;
 using osu.Framework.Graphics.Textures;
-using osu.Framework.Platform;
 using osu.Game.Graphics;
 using osu.Game.Graphics.Sprites;
 using osu.Game.Tournament.Components;
@@ -25,7 +24,7 @@ namespace osu.Game.Tournament.Screens.TeamIntro
         private readonly Bindable<TournamentTeam> currentTeam = new Bindable<TournamentTeam>();
 
         [BackgroundDependencyLoader]
-        private void load(Storage storage)
+        private void load()
         {
             RelativeSizeAxes = Axes.Both;
 
diff --git a/osu.Game.Tournament/Screens/TeamIntro/TeamIntroScreen.cs b/osu.Game.Tournament/Screens/TeamIntro/TeamIntroScreen.cs
index 74957cbca5..ef6f0b32ff 100644
--- a/osu.Game.Tournament/Screens/TeamIntro/TeamIntroScreen.cs
+++ b/osu.Game.Tournament/Screens/TeamIntro/TeamIntroScreen.cs
@@ -5,7 +5,6 @@ using osu.Framework.Allocation;
 using osu.Framework.Bindables;
 using osu.Framework.Graphics;
 using osu.Framework.Graphics.Containers;
-using osu.Framework.Platform;
 using osu.Game.Tournament.Components;
 using osu.Game.Tournament.Models;
 using osuTK;
@@ -17,7 +16,7 @@ namespace osu.Game.Tournament.Screens.TeamIntro
         private Container mainContainer;
 
         [BackgroundDependencyLoader]
-        private void load(Storage storage)
+        private void load()
         {
             RelativeSizeAxes = Axes.Both;
 
diff --git a/osu.Game.Tournament/Screens/TeamWin/TeamWinScreen.cs b/osu.Game.Tournament/Screens/TeamWin/TeamWinScreen.cs
index ebe2908b74..11db7bfad9 100644
--- a/osu.Game.Tournament/Screens/TeamWin/TeamWinScreen.cs
+++ b/osu.Game.Tournament/Screens/TeamWin/TeamWinScreen.cs
@@ -5,7 +5,6 @@ using osu.Framework.Allocation;
 using osu.Framework.Bindables;
 using osu.Framework.Graphics;
 using osu.Framework.Graphics.Containers;
-using osu.Framework.Platform;
 using osu.Game.Graphics;
 using osu.Game.Tournament.Components;
 using osu.Game.Tournament.Models;
@@ -23,7 +22,7 @@ namespace osu.Game.Tournament.Screens.TeamWin
         private TourneyVideo redWinVideo;
 
         [BackgroundDependencyLoader]
-        private void load(LadderInfo ladder, Storage storage)
+        private void load()
         {
             RelativeSizeAxes = Axes.Both;
 
diff --git a/osu.Game.Tournament/TournamentGameBase.cs b/osu.Game.Tournament/TournamentGameBase.cs
index d08322a3e8..f318c8bd85 100644
--- a/osu.Game.Tournament/TournamentGameBase.cs
+++ b/osu.Game.Tournament/TournamentGameBase.cs
@@ -81,8 +81,9 @@ namespace osu.Game.Tournament
 
                 ladder ??= new LadderInfo();
 
-                ladder.Ruleset.Value = RulesetStore.GetRuleset(ladder.Ruleset.Value?.ShortName)
-                                       ?? RulesetStore.AvailableRulesets.First();
+                ladder.Ruleset.Value = ladder.Ruleset.Value != null
+                    ? RulesetStore.GetRuleset(ladder.Ruleset.Value.ShortName)
+                    : RulesetStore.AvailableRulesets.First();
 
                 bool addedInfo = false;
 
diff --git a/osu.Game.Tournament/TournamentSceneManager.cs b/osu.Game.Tournament/TournamentSceneManager.cs
index 914d1163ad..80a9c07cde 100644
--- a/osu.Game.Tournament/TournamentSceneManager.cs
+++ b/osu.Game.Tournament/TournamentSceneManager.cs
@@ -7,11 +7,9 @@ using osu.Framework.Allocation;
 using osu.Framework.Graphics;
 using osu.Framework.Graphics.Containers;
 using osu.Framework.Graphics.Shapes;
-using osu.Framework.Platform;
 using osu.Framework.Threading;
 using osu.Game.Graphics;
 using osu.Game.Tournament.Components;
-using osu.Game.Tournament.Models;
 using osu.Game.Tournament.Screens;
 using osu.Game.Tournament.Screens.Drawings;
 using osu.Game.Tournament.Screens.Editors;
@@ -52,7 +50,7 @@ namespace osu.Game.Tournament
         }
 
         [BackgroundDependencyLoader]
-        private void load(LadderInfo ladder, Storage storage)
+        private void load()
         {
             InternalChildren = new Drawable[]
             {
diff --git a/osu.Game/Beatmaps/Beatmap.cs b/osu.Game/Beatmaps/Beatmap.cs
index 435183fe92..2fa5a56042 100644
--- a/osu.Game/Beatmaps/Beatmap.cs
+++ b/osu.Game/Beatmaps/Beatmap.cs
@@ -28,7 +28,7 @@ namespace osu.Game.Beatmaps
                 difficulty = value;
 
                 if (beatmapInfo != null)
-                    beatmapInfo.BaseDifficulty = difficulty.Clone();
+                    beatmapInfo.Difficulty = difficulty.Clone();
             }
         }
 
@@ -41,8 +41,8 @@ namespace osu.Game.Beatmaps
             {
                 beatmapInfo = value;
 
-                if (beatmapInfo?.BaseDifficulty != null)
-                    Difficulty = beatmapInfo.BaseDifficulty.Clone();
+                if (beatmapInfo?.Difficulty != null)
+                    Difficulty = beatmapInfo.Difficulty.Clone();
             }
         }
 
@@ -54,15 +54,15 @@ namespace osu.Game.Beatmaps
                 {
                     Artist = @"Unknown",
                     Title = @"Unknown",
-                    AuthorString = @"Unknown Creator",
+                    Author = { Username = @"Unknown Creator" },
                 },
                 DifficultyName = @"Normal",
-                BaseDifficulty = Difficulty,
+                Difficulty = Difficulty,
             };
         }
 
         [JsonIgnore]
-        public BeatmapMetadata Metadata => BeatmapInfo?.Metadata ?? BeatmapInfo?.BeatmapSet?.Metadata;
+        public BeatmapMetadata Metadata => BeatmapInfo.Metadata;
 
         public ControlPointInfo ControlPointInfo { get; set; } = new ControlPointInfo();
 
diff --git a/osu.Game/Beatmaps/BeatmapDifficulty.cs b/osu.Game/Beatmaps/BeatmapDifficulty.cs
index 65d1fb8286..7e0462f1e8 100644
--- a/osu.Game/Beatmaps/BeatmapDifficulty.cs
+++ b/osu.Game/Beatmaps/BeatmapDifficulty.cs
@@ -1,27 +1,29 @@
 // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
 // See the LICENCE file in the repository root for full licence text.
 
-using System;
-using osu.Game.Database;
+using osu.Framework.Testing;
+using Realms;
+
+#nullable enable
 
 namespace osu.Game.Beatmaps
 {
-    public class BeatmapDifficulty : IHasPrimaryKey, IBeatmapDifficultyInfo
+    [ExcludeFromDynamicCompile]
+    [MapTo("BeatmapDifficulty")]
+    public class BeatmapDifficulty : EmbeddedObject, IBeatmapDifficultyInfo
     {
         /// <summary>
         /// The default value used for all difficulty settings except <see cref="SliderMultiplier"/> and <see cref="SliderTickRate"/>.
         /// </summary>
         public const float DEFAULT_DIFFICULTY = 5;
 
-        public int ID { get; set; }
+        public float DrainRate { get; set; } = IBeatmapDifficultyInfo.DEFAULT_DIFFICULTY;
+        public float CircleSize { get; set; } = IBeatmapDifficultyInfo.DEFAULT_DIFFICULTY;
+        public float OverallDifficulty { get; set; } = IBeatmapDifficultyInfo.DEFAULT_DIFFICULTY;
+        public float ApproachRate { get; set; } = IBeatmapDifficultyInfo.DEFAULT_DIFFICULTY;
 
-        public bool IsManaged => ID > 0;
-
-        public float DrainRate { get; set; } = DEFAULT_DIFFICULTY;
-        public float CircleSize { get; set; } = DEFAULT_DIFFICULTY;
-        public float OverallDifficulty { get; set; } = DEFAULT_DIFFICULTY;
-
-        private float? approachRate;
+        public double SliderMultiplier { get; set; } = 1;
+        public double SliderTickRate { get; set; } = 1;
 
         public BeatmapDifficulty()
         {
@@ -32,23 +34,20 @@ namespace osu.Game.Beatmaps
             CopyFrom(source);
         }
 
-        public float ApproachRate
-        {
-            get => approachRate ?? OverallDifficulty;
-            set => approachRate = value;
-        }
-
-        public double SliderMultiplier { get; set; } = 1;
-        public double SliderTickRate { get; set; } = 1;
-
         /// <summary>
         /// Returns a shallow-clone of this <see cref="BeatmapDifficulty"/>.
         /// </summary>
-        public BeatmapDifficulty Clone()
+        public virtual BeatmapDifficulty Clone() => new BeatmapDifficulty(this);
+
+        public virtual void CopyTo(BeatmapDifficulty difficulty)
         {
-            var diff = (BeatmapDifficulty)Activator.CreateInstance(GetType());
-            CopyTo(diff);
-            return diff;
+            difficulty.ApproachRate = ApproachRate;
+            difficulty.DrainRate = DrainRate;
+            difficulty.CircleSize = CircleSize;
+            difficulty.OverallDifficulty = OverallDifficulty;
+
+            difficulty.SliderMultiplier = SliderMultiplier;
+            difficulty.SliderTickRate = SliderTickRate;
         }
 
         public virtual void CopyFrom(IBeatmapDifficultyInfo other)
@@ -61,16 +60,5 @@ namespace osu.Game.Beatmaps
             SliderMultiplier = other.SliderMultiplier;
             SliderTickRate = other.SliderTickRate;
         }
-
-        public virtual void CopyTo(BeatmapDifficulty other)
-        {
-            other.ApproachRate = ApproachRate;
-            other.DrainRate = DrainRate;
-            other.CircleSize = CircleSize;
-            other.OverallDifficulty = OverallDifficulty;
-
-            other.SliderMultiplier = SliderMultiplier;
-            other.SliderTickRate = SliderTickRate;
-        }
     }
 }
diff --git a/osu.Game/Beatmaps/BeatmapDifficultyCache.cs b/osu.Game/Beatmaps/BeatmapDifficultyCache.cs
index f760c25170..f102daeef5 100644
--- a/osu.Game/Beatmaps/BeatmapDifficultyCache.cs
+++ b/osu.Game/Beatmaps/BeatmapDifficultyCache.cs
@@ -136,7 +136,7 @@ namespace osu.Game.Beatmaps
             var localRulesetInfo = rulesetInfo as RulesetInfo;
 
             // Difficulty can only be computed if the beatmap and ruleset are locally available.
-            if (localBeatmapInfo?.IsManaged != true || localRulesetInfo == null)
+            if (localBeatmapInfo == null || localRulesetInfo == null)
             {
                 // If not, fall back to the existing star difficulty (e.g. from an online source).
                 return Task.FromResult<StarDifficulty?>(new StarDifficulty(beatmapInfo.StarRating, (beatmapInfo as IBeatmapOnlineInfo)?.MaxCombo ?? 0));
diff --git a/osu.Game/Beatmaps/BeatmapInfo.cs b/osu.Game/Beatmaps/BeatmapInfo.cs
index 4175d7ff6b..cddd7e9b30 100644
--- a/osu.Game/Beatmaps/BeatmapInfo.cs
+++ b/osu.Game/Beatmaps/BeatmapInfo.cs
@@ -2,102 +2,119 @@
 // See the LICENCE file in the repository root for full licence text.
 
 using System;
-using System.Collections.Generic;
-using System.ComponentModel.DataAnnotations;
-using System.ComponentModel.DataAnnotations.Schema;
+using System.Linq;
+using JetBrains.Annotations;
 using Newtonsoft.Json;
 using osu.Framework.Testing;
 using osu.Game.Database;
+using osu.Game.Models;
 using osu.Game.Online.API.Requests.Responses;
 using osu.Game.Rulesets;
 using osu.Game.Scoring;
+using Realms;
+
+#nullable enable
 
 namespace osu.Game.Beatmaps
 {
+    /// <summary>
+    /// A single beatmap difficulty.
+    /// </summary>
     [ExcludeFromDynamicCompile]
     [Serializable]
-    public class BeatmapInfo : IEquatable<BeatmapInfo>, IHasPrimaryKey, IBeatmapInfo
+    [MapTo("Beatmap")]
+    public class BeatmapInfo : RealmObject, IHasGuidPrimaryKey, IBeatmapInfo, IEquatable<BeatmapInfo>
     {
-        public int ID { get; set; }
+        [PrimaryKey]
+        public Guid ID { get; set; } = Guid.NewGuid();
 
-        public bool IsManaged => ID > 0;
+        public string DifficultyName { get; set; } = string.Empty;
 
-        public int BeatmapVersion;
+        public RulesetInfo Ruleset { get; set; }
 
-        private int? onlineID;
-
-        [JsonProperty("id")]
-        [Column("OnlineBeatmapID")]
-        public int? OnlineID
-        {
-            get => onlineID;
-            set => onlineID = value > 0 ? value : null;
-        }
-
-        [JsonIgnore]
-        public int BeatmapSetInfoID { get; set; }
-
-        public BeatmapOnlineStatus Status { get; set; } = BeatmapOnlineStatus.None;
-
-        [Required]
-        public BeatmapSetInfo BeatmapSet { get; set; }
+        public BeatmapDifficulty Difficulty { get; set; }
 
         public BeatmapMetadata Metadata { get; set; }
 
-        [JsonIgnore]
-        public int BaseDifficultyID { get; set; }
+        [Backlink(nameof(ScoreInfo.BeatmapInfo))]
+        public IQueryable<ScoreInfo> Scores { get; } = null!;
 
-        public BeatmapDifficulty BaseDifficulty { get; set; }
+        public BeatmapInfo(RulesetInfo ruleset, BeatmapDifficulty difficulty, BeatmapMetadata metadata)
+        {
+            Ruleset = ruleset;
+            Difficulty = difficulty;
+            Metadata = metadata;
+        }
 
-        [NotMapped]
-        public APIBeatmap OnlineInfo { get; set; }
+        [UsedImplicitly]
+        public BeatmapInfo() // TODO: consider removing this and migrating all usages to ctor with parameters.
+        {
+            Ruleset = new RulesetInfo
+            {
+                OnlineID = 0,
+                ShortName = @"osu",
+                Name = @"null placeholder ruleset"
+            };
+            Difficulty = new BeatmapDifficulty();
+            Metadata = new BeatmapMetadata();
+        }
 
-        [NotMapped]
-        public int? MaxCombo { get; set; }
+        public BeatmapSetInfo? BeatmapSet { get; set; }
+
+        [Ignored]
+        public RealmNamedFileUsage? File => BeatmapSet?.Files.FirstOrDefault(f => f.File.Hash == Hash);
+
+        [Ignored]
+        public BeatmapOnlineStatus Status
+        {
+            get => (BeatmapOnlineStatus)StatusInt;
+            set => StatusInt = (int)value;
+        }
+
+        [MapTo(nameof(Status))]
+        public int StatusInt { get; set; } = (int)BeatmapOnlineStatus.None;
+
+        [Indexed]
+        public int OnlineID { get; set; } = -1;
 
-        /// <summary>
-        /// The playable length in milliseconds of this beatmap.
-        /// </summary>
         public double Length { get; set; }
 
-        /// <summary>
-        /// The most common BPM of this beatmap.
-        /// </summary>
         public double BPM { get; set; }
 
-        public string Path { get; set; }
+        public string Hash { get; set; } = string.Empty;
 
-        [JsonProperty("file_sha2")]
-        public string Hash { get; set; }
+        public double StarRating { get; set; }
+
+        public string MD5Hash { get; set; } = string.Empty;
 
         [JsonIgnore]
         public bool Hidden { get; set; }
 
-        /// <summary>
-        /// MD5 is kept for legacy support (matching against replays, osu-web-10 etc.).
-        /// </summary>
-        [JsonProperty("file_md5")]
-        public string MD5Hash { get; set; }
+        #region Properties we may not want persisted (but also maybe no harm?)
 
-        // General
         public double AudioLeadIn { get; set; }
+
         public float StackLeniency { get; set; } = 0.7f;
+
         public bool SpecialStyle { get; set; }
 
-        public int RulesetID { get; set; }
-
-        public RulesetInfo Ruleset { get; set; }
-
         public bool LetterboxInBreaks { get; set; }
+
         public bool WidescreenStoryboard { get; set; }
+
         public bool EpilepsyWarning { get; set; }
 
-        /// <summary>
-        /// Whether or not sound samples should change rate when playing with speed-changing mods.
-        /// TODO: only read/write supported for now, requires implementation in gameplay.
-        /// </summary>
         public bool SamplesMatchPlaybackRate { get; set; }
 
+        public double DistanceSpacing { get; set; }
+
+        public int BeatDivisor { get; set; }
+
+        public int GridSize { get; set; }
+
+        public double TimelineZoom { get; set; }
+
+        [Ignored]
         public CountdownType Countdown { get; set; } = CountdownType.Normal;
 
         /// <summary>
@@ -105,77 +122,75 @@ namespace osu.Game.Beatmaps
         /// </summary>
         public int CountdownOffset { get; set; }
 
-        [NotMapped]
-        public int[] Bookmarks { get; set; } = Array.Empty<int>();
+        #endregion
 
-        public double DistanceSpacing { get; set; }
-        public int BeatDivisor { get; set; }
-        public int GridSize { get; set; }
-        public double TimelineZoom { get; set; }
-
-        // Metadata
-        [Column("Version")]
-        public string DifficultyName { get; set; }
-
-        [JsonProperty("difficulty_rating")]
-        [Column("StarDifficulty")]
-        public double StarRating { get; set; }
-
-        /// <summary>
-        /// Currently only populated for beatmap deletion. Use <see cref="ScoreManager"/> to query scores.
-        /// </summary>
-        public List<ScoreInfo> Scores { get; set; }
-
-        [JsonIgnore]
-        public DifficultyRating DifficultyRating => BeatmapDifficultyCache.GetDifficultyRating(StarRating);
-
-        public override string ToString() => this.GetDisplayTitle();
-
-        public bool Equals(BeatmapInfo other)
+        public bool Equals(BeatmapInfo? other)
         {
             if (ReferenceEquals(this, other)) return true;
             if (other == null) return false;
 
-            if (ID != 0 && other.ID != 0)
-                return ID == other.ID;
-
-            return false;
+            return ID == other.ID;
         }
 
-        public bool Equals(IBeatmapInfo other) => other is BeatmapInfo b && Equals(b);
+        public bool Equals(IBeatmapInfo? other) => other is BeatmapInfo b && Equals(b);
 
-        public bool AudioEquals(BeatmapInfo other) => other != null && BeatmapSet != null && other.BeatmapSet != null &&
-                                                      BeatmapSet.Hash == other.BeatmapSet.Hash &&
-                                                      (Metadata ?? BeatmapSet.Metadata).AudioFile == (other.Metadata ?? other.BeatmapSet.Metadata).AudioFile;
+        public bool AudioEquals(BeatmapInfo? other) => other != null
+                                                       && BeatmapSet != null
+                                                       && other.BeatmapSet != null
+                                                       && BeatmapSet.Hash == other.BeatmapSet.Hash
+                                                       && Metadata.AudioFile == other.Metadata.AudioFile;
 
-        public bool BackgroundEquals(BeatmapInfo other) => other != null && BeatmapSet != null && other.BeatmapSet != null &&
-                                                           BeatmapSet.Hash == other.BeatmapSet.Hash &&
-                                                           (Metadata ?? BeatmapSet.Metadata).BackgroundFile == (other.Metadata ?? other.BeatmapSet.Metadata).BackgroundFile;
+        public bool BackgroundEquals(BeatmapInfo? other) => other != null
+                                                            && BeatmapSet != null
+                                                            && other.BeatmapSet != null
+                                                            && BeatmapSet.Hash == other.BeatmapSet.Hash
+                                                            && Metadata.BackgroundFile == other.Metadata.BackgroundFile;
 
-        /// <summary>
-        /// Returns a shallow-clone of this <see cref="BeatmapInfo"/>.
-        /// </summary>
-        public BeatmapInfo Clone() => (BeatmapInfo)MemberwiseClone();
-
-        #region Implementation of IHasOnlineID
-
-        int IHasOnlineID<int>.OnlineID => OnlineID ?? -1;
-
-        #endregion
-
-        #region Implementation of IBeatmapInfo
-
-        [JsonIgnore]
-        IBeatmapMetadataInfo IBeatmapInfo.Metadata => Metadata ?? BeatmapSet?.Metadata ?? new BeatmapMetadata();
-
-        [JsonIgnore]
-        IBeatmapDifficultyInfo IBeatmapInfo.Difficulty => BaseDifficulty;
-
-        [JsonIgnore]
-        IBeatmapSetInfo IBeatmapInfo.BeatmapSet => BeatmapSet;
-
-        [JsonIgnore]
+        IBeatmapMetadataInfo IBeatmapInfo.Metadata => Metadata;
+        IBeatmapSetInfo? IBeatmapInfo.BeatmapSet => BeatmapSet;
         IRulesetInfo IBeatmapInfo.Ruleset => Ruleset;
+        IBeatmapDifficultyInfo IBeatmapInfo.Difficulty => Difficulty;
+
+        #region Compatibility properties
+
+        [Ignored]
+        public int RulesetID
+        {
+            get => Ruleset.OnlineID;
+            set
+            {
+                if (!string.IsNullOrEmpty(Ruleset.InstantiationInfo))
+                    throw new InvalidOperationException($"Cannot set a {nameof(RulesetID)} when {nameof(Ruleset)} is already set to an actual ruleset.");
+
+                Ruleset.OnlineID = value;
+            }
+        }
+
+        [Ignored]
+        [Obsolete("Use BeatmapInfo.Difficulty instead.")] // can be removed 20220719
+        public BeatmapDifficulty BaseDifficulty
+        {
+            get => Difficulty;
+            set => Difficulty = value;
+        }
+
+        [Ignored]
+        public string? Path => File?.Filename;
+
+        [Ignored]
+        public APIBeatmap? OnlineInfo { get; set; }
+
+        [Ignored]
+        public int? MaxCombo { get; set; }
+
+        [Ignored]
+        public int[] Bookmarks { get; set; } = Array.Empty<int>();
+
+        public int BeatmapVersion;
+
+        public BeatmapInfo Clone() => (BeatmapInfo)this.Detach().MemberwiseClone();
+
+        public override string ToString() => this.GetDisplayTitle();
 
         #endregion
     }
diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs
index ed7fe0bc91..ee649ad960 100644
--- a/osu.Game/Beatmaps/BeatmapManager.cs
+++ b/osu.Game/Beatmaps/BeatmapManager.cs
@@ -8,7 +8,6 @@ using System.Linq;
 using System.Linq.Expressions;
 using System.Threading;
 using System.Threading.Tasks;
-using JetBrains.Annotations;
 using osu.Framework.Audio;
 using osu.Framework.Audio.Track;
 using osu.Framework.Extensions;
@@ -16,13 +15,16 @@ using osu.Framework.IO.Stores;
 using osu.Framework.Platform;
 using osu.Framework.Testing;
 using osu.Game.Database;
-using osu.Game.IO;
 using osu.Game.IO.Archives;
+using osu.Game.Models;
 using osu.Game.Online.API;
 using osu.Game.Online.API.Requests.Responses;
 using osu.Game.Overlays.Notifications;
 using osu.Game.Rulesets;
 using osu.Game.Skinning;
+using osu.Game.Stores;
+
+#nullable enable
 
 namespace osu.Game.Beatmaps
 {
@@ -30,41 +32,46 @@ namespace osu.Game.Beatmaps
     /// Handles general operations related to global beatmap management.
     /// </summary>
     [ExcludeFromDynamicCompile]
-    public class BeatmapManager : IModelManager<BeatmapSetInfo>, IModelFileManager<BeatmapSetInfo, BeatmapSetFileInfo>, IModelImporter<BeatmapSetInfo>, IWorkingBeatmapCache, IDisposable
+    public class BeatmapManager : IModelManager<BeatmapSetInfo>, IModelFileManager<BeatmapSetInfo, RealmNamedFileUsage>, IModelImporter<BeatmapSetInfo>, IWorkingBeatmapCache, IDisposable
     {
         public ITrackStore BeatmapTrackStore { get; }
 
         private readonly BeatmapModelManager beatmapModelManager;
 
         private readonly WorkingBeatmapCache workingBeatmapCache;
-        private readonly BeatmapOnlineLookupQueue onlineBeatmapLookupQueue;
+        private readonly BeatmapOnlineLookupQueue? onlineBeatmapLookupQueue;
 
-        public BeatmapManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, IAPIProvider api, [NotNull] AudioManager audioManager, IResourceStore<byte[]> gameResources, GameHost host = null, WorkingBeatmap defaultBeatmap = null, bool performOnlineLookups = false)
+        private readonly RealmContextFactory contextFactory;
+
+        public BeatmapManager(Storage storage, RealmContextFactory contextFactory, RulesetStore rulesets, IAPIProvider? api, AudioManager audioManager, IResourceStore<byte[]> gameResources, GameHost? host = null, WorkingBeatmap? defaultBeatmap = null, bool performOnlineLookups = false)
         {
-            var userResources = new FileStore(contextFactory, storage).Store;
-
-            BeatmapTrackStore = audioManager.GetTrackStore(userResources);
-
-            beatmapModelManager = CreateBeatmapModelManager(storage, contextFactory, rulesets, api, host);
-            workingBeatmapCache = CreateWorkingBeatmapCache(audioManager, gameResources, userResources, defaultBeatmap, host);
-
-            workingBeatmapCache.BeatmapManager = beatmapModelManager;
-            beatmapModelManager.WorkingBeatmapCache = workingBeatmapCache;
+            this.contextFactory = contextFactory;
 
             if (performOnlineLookups)
             {
+                if (api == null)
+                    throw new ArgumentNullException(nameof(api), "API must be provided if online lookups are required.");
+
                 onlineBeatmapLookupQueue = new BeatmapOnlineLookupQueue(api, storage);
-                beatmapModelManager.OnlineLookupQueue = onlineBeatmapLookupQueue;
             }
+
+            var userResources = new RealmFileStore(contextFactory, storage).Store;
+
+            BeatmapTrackStore = audioManager.GetTrackStore(userResources);
+
+            beatmapModelManager = CreateBeatmapModelManager(storage, contextFactory, rulesets, onlineBeatmapLookupQueue);
+            workingBeatmapCache = CreateWorkingBeatmapCache(audioManager, gameResources, userResources, defaultBeatmap, host);
+
+            beatmapModelManager.WorkingBeatmapCache = workingBeatmapCache;
         }
 
-        protected virtual WorkingBeatmapCache CreateWorkingBeatmapCache(AudioManager audioManager, IResourceStore<byte[]> resources, IResourceStore<byte[]> storage, WorkingBeatmap defaultBeatmap, GameHost host)
+        protected virtual WorkingBeatmapCache CreateWorkingBeatmapCache(AudioManager audioManager, IResourceStore<byte[]> resources, IResourceStore<byte[]> storage, WorkingBeatmap? defaultBeatmap, GameHost? host)
         {
             return new WorkingBeatmapCache(BeatmapTrackStore, audioManager, resources, storage, defaultBeatmap, host);
         }
 
-        protected virtual BeatmapModelManager CreateBeatmapModelManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, IAPIProvider api, GameHost host) =>
-            new BeatmapModelManager(storage, contextFactory, rulesets, host);
+        protected virtual BeatmapModelManager CreateBeatmapModelManager(Storage storage, RealmContextFactory contextFactory, RulesetStore rulesets, BeatmapOnlineLookupQueue? onlineLookupQueue) =>
+            new BeatmapModelManager(contextFactory, storage, onlineLookupQueue);
 
         /// <summary>
         /// Create a new <see cref="WorkingBeatmap"/>.
@@ -73,17 +80,20 @@ namespace osu.Game.Beatmaps
         {
             var metadata = new BeatmapMetadata
             {
-                Author = user,
+                Author = new RealmUser
+                {
+                    OnlineID = user.OnlineID,
+                    Username = user.Username,
+                }
             };
 
-            var set = new BeatmapSetInfo
+            var beatmapSet = new BeatmapSetInfo
             {
-                Metadata = metadata,
                 Beatmaps =
                 {
                     new BeatmapInfo
                     {
-                        BaseDifficulty = new BeatmapDifficulty(),
+                        Difficulty = new BeatmapDifficulty(),
                         Ruleset = ruleset,
                         Metadata = metadata,
                         WidescreenStoryboard = true,
@@ -92,30 +102,92 @@ namespace osu.Game.Beatmaps
                 }
             };
 
-            var imported = beatmapModelManager.Import(set).GetResultSafely().Value;
+            foreach (BeatmapInfo b in beatmapSet.Beatmaps)
+                b.BeatmapSet = beatmapSet;
 
-            return GetWorkingBeatmap(imported.Beatmaps.First());
+            var imported = beatmapModelManager.Import(beatmapSet).GetResultSafely();
+
+            if (imported == null)
+                throw new InvalidOperationException("Failed to import new beatmap");
+
+            return imported.PerformRead(s => GetWorkingBeatmap(s.Beatmaps.First()));
+        }
+
+        /// <summary>
+        /// Delete a beatmap difficulty.
+        /// </summary>
+        /// <param name="beatmapInfo">The beatmap difficulty to hide.</param>
+        public void Hide(BeatmapInfo beatmapInfo)
+        {
+            using (var realm = contextFactory.CreateContext())
+            using (var transaction = realm.BeginWrite())
+            {
+                if (!beatmapInfo.IsManaged)
+                    beatmapInfo = realm.Find<BeatmapInfo>(beatmapInfo.ID);
+
+                beatmapInfo.Hidden = true;
+                transaction.Commit();
+            }
+        }
+
+        /// <summary>
+        /// Restore a beatmap difficulty.
+        /// </summary>
+        /// <param name="beatmapInfo">The beatmap difficulty to restore.</param>
+        public void Restore(BeatmapInfo beatmapInfo)
+        {
+            using (var realm = contextFactory.CreateContext())
+            using (var transaction = realm.BeginWrite())
+            {
+                if (!beatmapInfo.IsManaged)
+                    beatmapInfo = realm.Find<BeatmapInfo>(beatmapInfo.ID);
+
+                beatmapInfo.Hidden = false;
+                transaction.Commit();
+            }
+        }
+
+        public void RestoreAll()
+        {
+            using (var realm = contextFactory.CreateContext())
+            using (var transaction = realm.BeginWrite())
+            {
+                foreach (var beatmap in realm.All<BeatmapInfo>().Where(b => b.Hidden))
+                    beatmap.Hidden = false;
+
+                transaction.Commit();
+            }
+        }
+
+        /// <summary>
+        /// Returns a list of all usable <see cref="BeatmapSetInfo"/>s.
+        /// </summary>
+        /// <returns>A list of available <see cref="BeatmapSetInfo"/>.</returns>
+        public List<BeatmapSetInfo> GetAllUsableBeatmapSets()
+        {
+            using (var context = contextFactory.CreateContext())
+                return context.All<BeatmapSetInfo>().Where(b => !b.DeletePending).Detach();
+        }
+
+        /// <summary>
+        /// Perform a lookup query on available <see cref="BeatmapSetInfo"/>s.
+        /// </summary>
+        /// <param name="query">The query.</param>
+        /// <returns>The first result for the provided query, or null if no results were found.</returns>
+        public ILive<BeatmapSetInfo>? QueryBeatmapSet(Expression<Func<BeatmapSetInfo, bool>> query)
+        {
+            using (var context = contextFactory.CreateContext())
+                return context.All<BeatmapSetInfo>().FirstOrDefault(query)?.ToLive(contextFactory);
         }
 
         #region Delegation to BeatmapModelManager (methods which previously existed locally).
 
         /// <summary>
-        /// Fired when a single difficulty has been hidden.
+        /// Perform a lookup query on available <see cref="BeatmapInfo"/>s.
         /// </summary>
-        public event Action<BeatmapInfo> BeatmapHidden
-        {
-            add => beatmapModelManager.BeatmapHidden += value;
-            remove => beatmapModelManager.BeatmapHidden -= value;
-        }
-
-        /// <summary>
-        /// Fired when a single difficulty has been restored.
-        /// </summary>
-        public event Action<BeatmapInfo> BeatmapRestored
-        {
-            add => beatmapModelManager.BeatmapRestored += value;
-            remove => beatmapModelManager.BeatmapRestored -= value;
-        }
+        /// <param name="query">The query.</param>
+        /// <returns>The first result for the provided query, or null if no results were found.</returns>
+        public BeatmapInfo? QueryBeatmap(Expression<Func<BeatmapInfo, bool>> query) => beatmapModelManager.QueryBeatmap(query)?.Detach();
 
         /// <summary>
         /// Saves an <see cref="IBeatmap"/> file against a given <see cref="BeatmapInfo"/>.
@@ -123,52 +195,9 @@ namespace osu.Game.Beatmaps
         /// <param name="info">The <see cref="BeatmapInfo"/> to save the content against. The file referenced by <see cref="BeatmapInfo.Path"/> will be replaced.</param>
         /// <param name="beatmapContent">The <see cref="IBeatmap"/> content to write.</param>
         /// <param name="beatmapSkin">The beatmap <see cref="ISkin"/> content to write, null if to be omitted.</param>
-        public virtual void Save(BeatmapInfo info, IBeatmap beatmapContent, ISkin beatmapSkin = null) =>
+        public virtual void Save(BeatmapInfo info, IBeatmap beatmapContent, ISkin? beatmapSkin = null) =>
             beatmapModelManager.Save(info, beatmapContent, beatmapSkin);
 
-        /// <summary>
-        /// Returns a list of all usable <see cref="BeatmapSetInfo"/>s.
-        /// </summary>
-        /// <returns>A list of available <see cref="BeatmapSetInfo"/>.</returns>
-        public List<BeatmapSetInfo> GetAllUsableBeatmapSets(IncludedDetails includes = IncludedDetails.All, bool includeProtected = false) => beatmapModelManager.GetAllUsableBeatmapSets(includes, includeProtected);
-
-        /// <summary>
-        /// Returns a list of all usable <see cref="BeatmapSetInfo"/>s. Note that files are not populated.
-        /// </summary>
-        /// <param name="includes">The level of detail to include in the returned objects.</param>
-        /// <param name="includeProtected">Whether to include protected (system) beatmaps. These should not be included for gameplay playable use cases.</param>
-        /// <returns>A list of available <see cref="BeatmapSetInfo"/>.</returns>
-        public IEnumerable<BeatmapSetInfo> GetAllUsableBeatmapSetsEnumerable(IncludedDetails includes, bool includeProtected = false) => beatmapModelManager.GetAllUsableBeatmapSetsEnumerable(includes, includeProtected);
-
-        /// <summary>
-        /// Perform a lookup query on available <see cref="BeatmapSetInfo"/>s.
-        /// </summary>
-        /// <param name="query">The query.</param>
-        /// <param name="includes">The level of detail to include in the returned objects.</param>
-        /// <returns>Results from the provided query.</returns>
-        public IEnumerable<BeatmapSetInfo> QueryBeatmapSets(Expression<Func<BeatmapSetInfo, bool>> query, IncludedDetails includes = IncludedDetails.All) => beatmapModelManager.QueryBeatmapSets(query, includes);
-
-        /// <summary>
-        /// Perform a lookup query on available <see cref="BeatmapSetInfo"/>s.
-        /// </summary>
-        /// <param name="query">The query.</param>
-        /// <returns>The first result for the provided query, or null if no results were found.</returns>
-        public BeatmapSetInfo QueryBeatmapSet(Expression<Func<BeatmapSetInfo, bool>> query) => beatmapModelManager.QueryBeatmapSet(query);
-
-        /// <summary>
-        /// Perform a lookup query on available <see cref="BeatmapInfo"/>s.
-        /// </summary>
-        /// <param name="query">The query.</param>
-        /// <returns>Results from the provided query.</returns>
-        public IQueryable<BeatmapInfo> QueryBeatmaps(Expression<Func<BeatmapInfo, bool>> query) => beatmapModelManager.QueryBeatmaps(query);
-
-        /// <summary>
-        /// Perform a lookup query on available <see cref="BeatmapInfo"/>s.
-        /// </summary>
-        /// <param name="query">The query.</param>
-        /// <returns>The first result for the provided query, or null if no results were found.</returns>
-        public BeatmapInfo QueryBeatmap(Expression<Func<BeatmapInfo, bool>> query) => beatmapModelManager.QueryBeatmap(query);
-
         /// <summary>
         /// A default representation of a WorkingBeatmap to use when no beatmap is available.
         /// </summary>
@@ -182,18 +211,6 @@ namespace osu.Game.Beatmaps
             set => beatmapModelManager.PostNotification = value;
         }
 
-        /// <summary>
-        /// Delete a beatmap difficulty.
-        /// </summary>
-        /// <param name="beatmapInfo">The beatmap difficulty to hide.</param>
-        public void Hide(BeatmapInfo beatmapInfo) => beatmapModelManager.Hide(beatmapInfo);
-
-        /// <summary>
-        /// Restore a beatmap difficulty.
-        /// </summary>
-        /// <param name="beatmapInfo">The beatmap difficulty to restore.</param>
-        public void Restore(BeatmapInfo beatmapInfo) => beatmapModelManager.Restore(beatmapInfo);
-
         #endregion
 
         #region Implementation of IModelManager<BeatmapSetInfo>
@@ -203,23 +220,6 @@ namespace osu.Game.Beatmaps
             return beatmapModelManager.IsAvailableLocally(model);
         }
 
-        public event Action<BeatmapSetInfo> ItemUpdated
-        {
-            add => beatmapModelManager.ItemUpdated += value;
-            remove => beatmapModelManager.ItemUpdated -= value;
-        }
-
-        public event Action<BeatmapSetInfo> ItemRemoved
-        {
-            add => beatmapModelManager.ItemRemoved += value;
-            remove => beatmapModelManager.ItemRemoved -= value;
-        }
-
-        public void Update(BeatmapSetInfo item)
-        {
-            beatmapModelManager.Update(item);
-        }
-
         public bool Delete(BeatmapSetInfo item)
         {
             return beatmapModelManager.Delete(item);
@@ -230,6 +230,25 @@ namespace osu.Game.Beatmaps
             beatmapModelManager.Delete(items, silent);
         }
 
+        public void Delete(Expression<Func<BeatmapSetInfo, bool>>? filter = null, bool silent = false)
+        {
+            using (var context = contextFactory.CreateContext())
+            {
+                var items = context.All<BeatmapSetInfo>().Where(s => !s.DeletePending && !s.Protected);
+
+                if (filter != null)
+                    items = items.Where(filter);
+
+                beatmapModelManager.Delete(items.ToList(), silent);
+            }
+        }
+
+        public void UndeleteAll()
+        {
+            using (var context = contextFactory.CreateContext())
+                beatmapModelManager.Undelete(context.All<BeatmapSetInfo>().Where(s => s.DeletePending).ToList());
+        }
+
         public void Undelete(List<BeatmapSetInfo> items, bool silent = false)
         {
             beatmapModelManager.Undelete(items, silent);
@@ -259,17 +278,17 @@ namespace osu.Game.Beatmaps
             return beatmapModelManager.Import(notification, tasks);
         }
 
-        public Task<ILive<BeatmapSetInfo>> Import(ImportTask task, bool lowPriority = false, CancellationToken cancellationToken = default)
+        public Task<ILive<BeatmapSetInfo>?> Import(ImportTask task, bool lowPriority = false, CancellationToken cancellationToken = default)
         {
             return beatmapModelManager.Import(task, lowPriority, cancellationToken);
         }
 
-        public Task<ILive<BeatmapSetInfo>> Import(ArchiveReader archive, bool lowPriority = false, CancellationToken cancellationToken = default)
+        public Task<ILive<BeatmapSetInfo>?> Import(ArchiveReader archive, bool lowPriority = false, CancellationToken cancellationToken = default)
         {
             return beatmapModelManager.Import(archive, lowPriority, cancellationToken);
         }
 
-        public Task<ILive<BeatmapSetInfo>> Import(BeatmapSetInfo item, ArchiveReader archive = null, bool lowPriority = false, CancellationToken cancellationToken = default)
+        public Task<ILive<BeatmapSetInfo>?> Import(BeatmapSetInfo item, ArchiveReader? archive = null, bool lowPriority = false, CancellationToken cancellationToken = default)
         {
             return beatmapModelManager.Import(item, archive, lowPriority, cancellationToken);
         }
@@ -280,7 +299,32 @@ namespace osu.Game.Beatmaps
 
         #region Implementation of IWorkingBeatmapCache
 
-        public WorkingBeatmap GetWorkingBeatmap(BeatmapInfo importedBeatmap) => workingBeatmapCache.GetWorkingBeatmap(importedBeatmap);
+        public WorkingBeatmap GetWorkingBeatmap(BeatmapInfo? importedBeatmap)
+        {
+            // Detached sets don't come with files.
+            // If we seem to be missing files, now is a good time to re-fetch.
+            if (importedBeatmap?.BeatmapSet?.Files.Count == 0)
+            {
+                using (var realm = contextFactory.CreateContext())
+                {
+                    var refetch = realm.Find<BeatmapInfo>(importedBeatmap.ID)?.Detach();
+
+                    if (refetch != null)
+                        importedBeatmap = refetch;
+                }
+            }
+
+            return workingBeatmapCache.GetWorkingBeatmap(importedBeatmap);
+        }
+
+        public WorkingBeatmap GetWorkingBeatmap(ILive<BeatmapInfo>? importedBeatmap)
+        {
+            WorkingBeatmap working = workingBeatmapCache.GetWorkingBeatmap(null);
+
+            importedBeatmap?.PerformRead(b => working = workingBeatmapCache.GetWorkingBeatmap(b));
+
+            return working;
+        }
 
         void IWorkingBeatmapCache.Invalidate(BeatmapSetInfo beatmapSetInfo) => workingBeatmapCache.Invalidate(beatmapSetInfo);
         void IWorkingBeatmapCache.Invalidate(BeatmapInfo beatmapInfo) => workingBeatmapCache.Invalidate(beatmapInfo);
@@ -289,12 +333,12 @@ namespace osu.Game.Beatmaps
 
         #region Implementation of IModelFileManager<in BeatmapSetInfo,in BeatmapSetFileInfo>
 
-        public void ReplaceFile(BeatmapSetInfo model, BeatmapSetFileInfo file, Stream contents)
+        public void ReplaceFile(BeatmapSetInfo model, RealmNamedFileUsage file, Stream contents)
         {
             beatmapModelManager.ReplaceFile(model, file, contents);
         }
 
-        public void DeleteFile(BeatmapSetInfo model, BeatmapSetFileInfo file)
+        public void DeleteFile(BeatmapSetInfo model, RealmNamedFileUsage file)
         {
             beatmapModelManager.DeleteFile(model, file);
         }
@@ -317,7 +361,7 @@ namespace osu.Game.Beatmaps
 
         #region Implementation of IPostImports<out BeatmapSetInfo>
 
-        public Action<IEnumerable<ILive<BeatmapSetInfo>>> PostImport
+        public Action<IEnumerable<ILive<BeatmapSetInfo>>>? PostImport
         {
             set => beatmapModelManager.PostImport = value;
         }
diff --git a/osu.Game/Beatmaps/BeatmapMetadata.cs b/osu.Game/Beatmaps/BeatmapMetadata.cs
index 5da0264893..a3385e3abe 100644
--- a/osu.Game/Beatmaps/BeatmapMetadata.cs
+++ b/osu.Game/Beatmaps/BeatmapMetadata.cs
@@ -2,13 +2,11 @@
 // See the LICENCE file in the repository root for full licence text.
 
 using System;
-using System.Collections.Generic;
-using System.ComponentModel.DataAnnotations.Schema;
 using Newtonsoft.Json;
 using osu.Framework.Testing;
-using osu.Game.Database;
-using osu.Game.Online.API.Requests.Responses;
+using osu.Game.Models;
 using osu.Game.Users;
+using Realms;
 
 #nullable enable
 
@@ -16,12 +14,9 @@ namespace osu.Game.Beatmaps
 {
     [ExcludeFromDynamicCompile]
     [Serializable]
-    public class BeatmapMetadata : IEquatable<BeatmapMetadata>, IHasPrimaryKey, IBeatmapMetadataInfo
+    [MapTo("BeatmapMetadata")]
+    public class BeatmapMetadata : RealmObject, IBeatmapMetadataInfo
     {
-        public int ID { get; set; }
-
-        public bool IsManaged => ID > 0;
-
         public string Title { get; set; } = string.Empty;
 
         [JsonProperty("title_unicode")]
@@ -32,39 +27,7 @@ namespace osu.Game.Beatmaps
         [JsonProperty("artist_unicode")]
         public string ArtistUnicode { get; set; } = string.Empty;
 
-        [JsonIgnore]
-        public List<BeatmapInfo> Beatmaps { get; set; } = new List<BeatmapInfo>();
-
-        [JsonIgnore]
-        public List<BeatmapSetInfo> BeatmapSets { get; set; } = new List<BeatmapSetInfo>();
-
-        /// <summary>
-        /// The author of the beatmaps in this set.
-        /// </summary>
-        [JsonIgnore]
-        public APIUser Author = new APIUser();
-
-        /// <summary>
-        /// Helper property to deserialize a username to <see cref="APIUser"/>.
-        /// </summary>
-        [JsonProperty(@"user_id")]
-        [Column("AuthorID")]
-        public int AuthorID
-        {
-            get => Author.Id; // This should not be used, but is required to make EF work correctly.
-            set => Author.Id = value;
-        }
-
-        /// <summary>
-        /// Helper property to deserialize a username to <see cref="APIUser"/>.
-        /// </summary>
-        [JsonProperty(@"creator")]
-        [Column("Author")]
-        public string AuthorString
-        {
-            get => Author.Username; // This should not be used, but is required to make EF work correctly.
-            set => Author.Username = value;
-        }
+        public RealmUser Author { get; set; } = new RealmUser(); // TODO: not sure we want to initialise this only to have it overwritten by retrieval.
 
         public string Source { get; set; } = string.Empty;
 
@@ -75,16 +38,13 @@ namespace osu.Game.Beatmaps
         /// The time in milliseconds to begin playing the track for preview purposes.
         /// If -1, the track should begin playing at 40% of its length.
         /// </summary>
-        public int PreviewTime { get; set; } = -1;
+        public int PreviewTime { get; set; }
 
         public string AudioFile { get; set; } = string.Empty;
-
         public string BackgroundFile { get; set; } = string.Empty;
 
-        public bool Equals(BeatmapMetadata other) => ((IBeatmapMetadataInfo)this).Equals(other);
+        IUser IBeatmapMetadataInfo.Author => Author;
 
         public override string ToString() => this.GetDisplayTitle();
-
-        IUser IBeatmapMetadataInfo.Author => Author;
     }
 }
diff --git a/osu.Game/Beatmaps/BeatmapMetadataInfoExtensions.cs b/osu.Game/Beatmaps/BeatmapMetadataInfoExtensions.cs
index 7aab6a7a9b..7e7d1babf0 100644
--- a/osu.Game/Beatmaps/BeatmapMetadataInfoExtensions.cs
+++ b/osu.Game/Beatmaps/BeatmapMetadataInfoExtensions.cs
@@ -40,7 +40,7 @@ namespace osu.Game.Beatmaps
         /// </summary>
         public static RomanisableString GetDisplayTitleRomanisable(this IBeatmapMetadataInfo metadataInfo, bool includeCreator = true)
         {
-            string author = !includeCreator || string.IsNullOrEmpty(metadataInfo.Author.Username) ? string.Empty : $"({metadataInfo.Author})";
+            string author = !includeCreator || string.IsNullOrEmpty(metadataInfo.Author.Username) ? string.Empty : $"({metadataInfo.Author.Username})";
             string artistUnicode = string.IsNullOrEmpty(metadataInfo.ArtistUnicode) ? metadataInfo.Artist : metadataInfo.ArtistUnicode;
             string titleUnicode = string.IsNullOrEmpty(metadataInfo.TitleUnicode) ? metadataInfo.Title : metadataInfo.TitleUnicode;
 
diff --git a/osu.Game/Beatmaps/BeatmapModelManager.cs b/osu.Game/Beatmaps/BeatmapModelManager.cs
index d0c41e0fb8..3822c6e121 100644
--- a/osu.Game/Beatmaps/BeatmapModelManager.cs
+++ b/osu.Game/Beatmaps/BeatmapModelManager.cs
@@ -3,194 +3,62 @@
 
 using System;
 using System.Collections.Generic;
+using System.Diagnostics;
 using System.IO;
 using System.Linq;
 using System.Linq.Expressions;
 using System.Text;
-using System.Threading;
-using System.Threading.Tasks;
-using Microsoft.EntityFrameworkCore;
-using osu.Framework.Audio.Track;
 using osu.Framework.Extensions;
-using osu.Framework.Graphics.Textures;
-using osu.Framework.Logging;
 using osu.Framework.Platform;
 using osu.Framework.Testing;
 using osu.Game.Beatmaps.Formats;
 using osu.Game.Database;
 using osu.Game.Extensions;
-using osu.Game.IO;
-using osu.Game.IO.Archives;
-using osu.Game.Rulesets;
-using osu.Game.Rulesets.Objects;
 using osu.Game.Skinning;
-using Decoder = osu.Game.Beatmaps.Formats.Decoder;
+using osu.Game.Stores;
+
+#nullable enable
 
 namespace osu.Game.Beatmaps
 {
-    /// <summary>
-    /// Handles ef-core storage of beatmaps.
-    /// </summary>
     [ExcludeFromDynamicCompile]
-    public class BeatmapModelManager : ArchiveModelManager<BeatmapSetInfo, BeatmapSetFileInfo>
+    public class BeatmapModelManager : BeatmapImporter
     {
-        /// <summary>
-        /// Fired when a single difficulty has been hidden.
-        /// </summary>
-        public event Action<BeatmapInfo> BeatmapHidden;
-
-        /// <summary>
-        /// Fired when a single difficulty has been restored.
-        /// </summary>
-        public event Action<BeatmapInfo> BeatmapRestored;
-
-        /// <summary>
-        /// An online lookup queue component which handles populating online beatmap metadata.
-        /// </summary>
-        public BeatmapOnlineLookupQueue OnlineLookupQueue { private get; set; }
-
         /// <summary>
         /// The game working beatmap cache, used to invalidate entries on changes.
         /// </summary>
-        public IWorkingBeatmapCache WorkingBeatmapCache { private get; set; }
+        public IWorkingBeatmapCache? WorkingBeatmapCache { private get; set; }
 
         public override IEnumerable<string> HandledExtensions => new[] { ".osz" };
 
         protected override string[] HashableFileTypes => new[] { ".osu" };
 
-        private readonly BeatmapStore beatmaps;
-        private readonly RulesetStore rulesets;
-
-        public BeatmapModelManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, GameHost host = null)
-            : base(storage, contextFactory, new BeatmapStore(contextFactory), host)
+        public BeatmapModelManager(RealmContextFactory contextFactory, Storage storage, BeatmapOnlineLookupQueue? onlineLookupQueue = null)
+            : base(contextFactory, storage, onlineLookupQueue)
         {
-            this.rulesets = rulesets;
-
-            beatmaps = (BeatmapStore)ModelStore;
-            beatmaps.BeatmapHidden += b => BeatmapHidden?.Invoke(b);
-            beatmaps.BeatmapRestored += b => BeatmapRestored?.Invoke(b);
-            beatmaps.ItemRemoved += b => WorkingBeatmapCache?.Invalidate(b);
-            beatmaps.ItemUpdated += obj => WorkingBeatmapCache?.Invalidate(obj);
         }
 
         protected override bool ShouldDeleteArchive(string path) => Path.GetExtension(path)?.ToLowerInvariant() == ".osz";
 
-        protected override async Task Populate(BeatmapSetInfo beatmapSet, ArchiveReader archive, CancellationToken cancellationToken = default)
-        {
-            if (archive != null)
-                beatmapSet.Beatmaps.AddRange(createBeatmapDifficulties(beatmapSet.Files));
-
-            foreach (BeatmapInfo b in beatmapSet.Beatmaps)
-            {
-                // remove metadata from difficulties where it matches the set
-                if (beatmapSet.Metadata.Equals(b.Metadata))
-                    b.Metadata = null;
-
-                b.BeatmapSet = beatmapSet;
-            }
-
-            validateOnlineIds(beatmapSet);
-
-            bool hadOnlineIDs = beatmapSet.Beatmaps.Any(b => b.OnlineID > 0);
-
-            if (OnlineLookupQueue != null)
-                await OnlineLookupQueue.UpdateAsync(beatmapSet, cancellationToken).ConfigureAwait(false);
-
-            // ensure at least one beatmap was able to retrieve or keep an online ID, else drop the set ID.
-            if (hadOnlineIDs && !beatmapSet.Beatmaps.Any(b => b.OnlineID > 0))
-            {
-                if (beatmapSet.OnlineID != null)
-                {
-                    beatmapSet.OnlineID = null;
-                    LogForModel(beatmapSet, "Disassociating beatmap set ID due to loss of all beatmap IDs");
-                }
-            }
-        }
-
-        protected override void PreImport(BeatmapSetInfo beatmapSet)
-        {
-            if (beatmapSet.Beatmaps.Any(b => b.BaseDifficulty == null))
-                throw new InvalidOperationException($"Cannot import {nameof(BeatmapInfo)} with null {nameof(BeatmapInfo.BaseDifficulty)}.");
-
-            // check if a set already exists with the same online id, delete if it does.
-            if (beatmapSet.OnlineID != null)
-            {
-                var existingSetWithSameOnlineID = beatmaps.ConsumableItems.FirstOrDefault(b => b.OnlineID == beatmapSet.OnlineID);
-
-                if (existingSetWithSameOnlineID != null)
-                {
-                    Delete(existingSetWithSameOnlineID);
-
-                    // in order to avoid a unique key constraint, immediately remove the online ID from the previous set.
-                    existingSetWithSameOnlineID.OnlineID = null;
-                    foreach (var b in existingSetWithSameOnlineID.Beatmaps)
-                        b.OnlineID = null;
-
-                    LogForModel(beatmapSet, $"Found existing beatmap set with same OnlineBeatmapSetID ({beatmapSet.OnlineID}). It has been deleted.");
-                }
-            }
-        }
-
-        private void validateOnlineIds(BeatmapSetInfo beatmapSet)
-        {
-            var beatmapIds = beatmapSet.Beatmaps.Where(b => b.OnlineID.HasValue).Select(b => b.OnlineID).ToList();
-
-            // ensure all IDs are unique
-            if (beatmapIds.GroupBy(b => b).Any(g => g.Count() > 1))
-            {
-                LogForModel(beatmapSet, "Found non-unique IDs, resetting...");
-                resetIds();
-                return;
-            }
-
-            // find any existing beatmaps in the database that have matching online ids
-            var existingBeatmaps = QueryBeatmaps(b => beatmapIds.Contains(b.OnlineID)).ToList();
-
-            if (existingBeatmaps.Count > 0)
-            {
-                // reset the import ids (to force a re-fetch) *unless* they match the candidate CheckForExisting set.
-                // we can ignore the case where the new ids are contained by the CheckForExisting set as it will either be used (import skipped) or deleted.
-                var existing = CheckForExisting(beatmapSet);
-
-                if (existing == null || existingBeatmaps.Any(b => !existing.Beatmaps.Contains(b)))
-                {
-                    LogForModel(beatmapSet, "Found existing import with IDs already, resetting...");
-                    resetIds();
-                }
-            }
-
-            void resetIds() => beatmapSet.Beatmaps.ForEach(b => b.OnlineID = null);
-        }
-
-        /// <summary>
-        /// Delete a beatmap difficulty.
-        /// </summary>
-        /// <param name="beatmapInfo">The beatmap difficulty to hide.</param>
-        public void Hide(BeatmapInfo beatmapInfo) => beatmaps.Hide(beatmapInfo);
-
-        /// <summary>
-        /// Restore a beatmap difficulty.
-        /// </summary>
-        /// <param name="beatmapInfo">The beatmap difficulty to restore.</param>
-        public void Restore(BeatmapInfo beatmapInfo) => beatmaps.Restore(beatmapInfo);
-
         /// <summary>
         /// Saves an <see cref="IBeatmap"/> file against a given <see cref="BeatmapInfo"/>.
         /// </summary>
         /// <param name="beatmapInfo">The <see cref="BeatmapInfo"/> to save the content against. The file referenced by <see cref="BeatmapInfo.Path"/> will be replaced.</param>
         /// <param name="beatmapContent">The <see cref="IBeatmap"/> content to write.</param>
         /// <param name="beatmapSkin">The beatmap <see cref="ISkin"/> content to write, null if to be omitted.</param>
-        public virtual void Save(BeatmapInfo beatmapInfo, IBeatmap beatmapContent, ISkin beatmapSkin = null)
+        public virtual void Save(BeatmapInfo beatmapInfo, IBeatmap beatmapContent, ISkin? beatmapSkin = null)
         {
             var setInfo = beatmapInfo.BeatmapSet;
 
+            Debug.Assert(setInfo != null);
+
             // Difficulty settings must be copied first due to the clone in `Beatmap<>.BeatmapInfo_Set`.
             // This should hopefully be temporary, assuming said clone is eventually removed.
 
             // Warning: The directionality here is important. Changes have to be copied *from* beatmapContent (which comes from editor and is being saved)
             // *to* the beatmapInfo (which is a database model and needs to receive values without the taiko slider velocity multiplier for correct operation).
             // CopyTo() will undo such adjustments, while CopyFrom() will not.
-            beatmapContent.Difficulty.CopyTo(beatmapInfo.BaseDifficulty);
+            beatmapContent.Difficulty.CopyTo(beatmapInfo.Difficulty);
 
             // All changes to metadata are made in the provided beatmapInfo, so this should be copied to the `IBeatmap` before encoding.
             beatmapContent.BeatmapInfo = beatmapInfo;
@@ -202,130 +70,25 @@ namespace osu.Game.Beatmaps
 
                 stream.Seek(0, SeekOrigin.Begin);
 
-                using (ContextFactory.GetForWrite())
-                {
-                    beatmapInfo = setInfo.Beatmaps.Single(b => b.Equals(beatmapInfo));
+                // AddFile generally handles updating/replacing files, but this is a case where the filename may have also changed so let's delete for simplicity.
+                var existingFileInfo = setInfo.Files.SingleOrDefault(f => string.Equals(f.Filename, beatmapInfo.Path, StringComparison.OrdinalIgnoreCase));
+                if (existingFileInfo != null)
+                    DeleteFile(setInfo, existingFileInfo);
 
-                    var metadata = beatmapInfo.Metadata ?? setInfo.Metadata;
+                beatmapInfo.MD5Hash = stream.ComputeMD5Hash();
+                beatmapInfo.Hash = stream.ComputeSHA2Hash();
 
-                    // grab the original file (or create a new one if not found).
-                    var fileInfo = setInfo.Files.SingleOrDefault(f => string.Equals(f.Filename, beatmapInfo.Path, StringComparison.OrdinalIgnoreCase)) ?? new BeatmapSetFileInfo();
-
-                    // metadata may have changed; update the path with the standard format.
-                    beatmapInfo.Path = $"{metadata.Artist} - {metadata.Title} ({metadata.Author}) [{beatmapInfo.DifficultyName}].osu".GetValidArchiveContentFilename();
-
-                    beatmapInfo.MD5Hash = stream.ComputeMD5Hash();
-
-                    // update existing or populate new file's filename.
-                    fileInfo.Filename = beatmapInfo.Path;
-
-                    stream.Seek(0, SeekOrigin.Begin);
-                    ReplaceFile(setInfo, fileInfo, stream);
-                }
+                AddFile(setInfo, stream, getFilename(beatmapInfo));
+                Update(setInfo);
             }
 
             WorkingBeatmapCache?.Invalidate(beatmapInfo);
         }
 
-        /// <summary>
-        /// Perform a lookup query on available <see cref="BeatmapSetInfo"/>s.
-        /// </summary>
-        /// <param name="query">The query.</param>
-        /// <returns>The first result for the provided query, or null if no results were found.</returns>
-        public BeatmapSetInfo QueryBeatmapSet(Expression<Func<BeatmapSetInfo, bool>> query) => beatmaps.ConsumableItems.AsNoTracking().FirstOrDefault(query);
-
-        protected override bool CanSkipImport(BeatmapSetInfo existing, BeatmapSetInfo import)
+        private static string getFilename(BeatmapInfo beatmapInfo)
         {
-            if (!base.CanSkipImport(existing, import))
-                return false;
-
-            return existing.Beatmaps.Any(b => b.OnlineID != null);
-        }
-
-        protected override bool CanReuseExisting(BeatmapSetInfo existing, BeatmapSetInfo import)
-        {
-            if (!base.CanReuseExisting(existing, import))
-                return false;
-
-            var existingIds = existing.Beatmaps.Select(b => b.OnlineID).OrderBy(i => i);
-            var importIds = import.Beatmaps.Select(b => b.OnlineID).OrderBy(i => i);
-
-            // force re-import if we are not in a sane state.
-            return existing.OnlineID == import.OnlineID && existingIds.SequenceEqual(importIds);
-        }
-
-        /// <summary>
-        /// Returns a list of all usable <see cref="BeatmapSetInfo"/>s.
-        /// </summary>
-        /// <returns>A list of available <see cref="BeatmapSetInfo"/>.</returns>
-        public List<BeatmapSetInfo> GetAllUsableBeatmapSets(IncludedDetails includes = IncludedDetails.All, bool includeProtected = false) =>
-            GetAllUsableBeatmapSetsEnumerable(includes, includeProtected).ToList();
-
-        /// <summary>
-        /// Returns a list of all usable <see cref="BeatmapSetInfo"/>s. Note that files are not populated.
-        /// </summary>
-        /// <param name="includes">The level of detail to include in the returned objects.</param>
-        /// <param name="includeProtected">Whether to include protected (system) beatmaps. These should not be included for gameplay playable use cases.</param>
-        /// <returns>A list of available <see cref="BeatmapSetInfo"/>.</returns>
-        public IEnumerable<BeatmapSetInfo> GetAllUsableBeatmapSetsEnumerable(IncludedDetails includes, bool includeProtected = false)
-        {
-            IQueryable<BeatmapSetInfo> queryable;
-
-            switch (includes)
-            {
-                case IncludedDetails.Minimal:
-                    queryable = beatmaps.BeatmapSetsOverview;
-                    break;
-
-                case IncludedDetails.AllButRuleset:
-                    queryable = beatmaps.BeatmapSetsWithoutRuleset;
-                    break;
-
-                case IncludedDetails.AllButFiles:
-                    queryable = beatmaps.BeatmapSetsWithoutFiles;
-                    break;
-
-                default:
-                    queryable = beatmaps.ConsumableItems;
-                    break;
-            }
-
-            // AsEnumerable used here to avoid applying the WHERE in sql. When done so, ef core 2.x uses an incorrect ORDER BY
-            // clause which causes queries to take 5-10x longer.
-            // TODO: remove if upgrading to EF core 3.x.
-            return queryable.AsEnumerable().Where(s => !s.DeletePending && (includeProtected || !s.Protected));
-        }
-
-        /// <summary>
-        /// Perform a lookup query on available <see cref="BeatmapSetInfo"/>s.
-        /// </summary>
-        /// <param name="query">The query.</param>
-        /// <param name="includes">The level of detail to include in the returned objects.</param>
-        /// <returns>Results from the provided query.</returns>
-        public IEnumerable<BeatmapSetInfo> QueryBeatmapSets(Expression<Func<BeatmapSetInfo, bool>> query, IncludedDetails includes = IncludedDetails.All)
-        {
-            IQueryable<BeatmapSetInfo> queryable;
-
-            switch (includes)
-            {
-                case IncludedDetails.Minimal:
-                    queryable = beatmaps.BeatmapSetsOverview;
-                    break;
-
-                case IncludedDetails.AllButRuleset:
-                    queryable = beatmaps.BeatmapSetsWithoutRuleset;
-                    break;
-
-                case IncludedDetails.AllButFiles:
-                    queryable = beatmaps.BeatmapSetsWithoutFiles;
-                    break;
-
-                default:
-                    queryable = beatmaps.ConsumableItems;
-                    break;
-            }
-
-            return queryable.AsNoTracking().Where(query);
+            var metadata = beatmapInfo.Metadata;
+            return $"{metadata.Artist} - {metadata.Title} ({metadata.Author}) [{beatmapInfo.DifficultyName}].osu".GetValidArchiveContentFilename();
         }
 
         /// <summary>
@@ -333,145 +96,19 @@ namespace osu.Game.Beatmaps
         /// </summary>
         /// <param name="query">The query.</param>
         /// <returns>The first result for the provided query, or null if no results were found.</returns>
-        public BeatmapInfo QueryBeatmap(Expression<Func<BeatmapInfo, bool>> query) => beatmaps.Beatmaps.AsNoTracking().FirstOrDefault(query);
-
-        /// <summary>
-        /// Perform a lookup query on available <see cref="BeatmapInfo"/>s.
-        /// </summary>
-        /// <param name="query">The query.</param>
-        /// <returns>Results from the provided query.</returns>
-        public IQueryable<BeatmapInfo> QueryBeatmaps(Expression<Func<BeatmapInfo, bool>> query) => beatmaps.Beatmaps.AsNoTracking().Where(query);
-
-        public override string HumanisedModelName => "beatmap";
-
-        protected override bool CheckLocalAvailability(BeatmapSetInfo model, IQueryable<BeatmapSetInfo> items)
-            => base.CheckLocalAvailability(model, items)
-               || (model.OnlineID != null && items.Any(b => b.OnlineID == model.OnlineID));
-
-        protected override BeatmapSetInfo CreateModel(ArchiveReader reader)
+        public BeatmapInfo? QueryBeatmap(Expression<Func<BeatmapInfo, bool>> query)
         {
-            // let's make sure there are actually .osu files to import.
-            string mapName = reader.Filenames.FirstOrDefault(f => f.EndsWith(".osu", StringComparison.OrdinalIgnoreCase));
+            using (var context = ContextFactory.CreateContext())
+                return context.All<BeatmapInfo>().FirstOrDefault(query)?.Detach();
+        }
 
-            if (string.IsNullOrEmpty(mapName))
+        public void Update(BeatmapSetInfo item)
+        {
+            using (var realm = ContextFactory.CreateContext())
             {
-                Logger.Log($"No beatmap files found in the beatmap archive ({reader.Name}).", LoggingTarget.Database);
-                return null;
+                var existing = realm.Find<BeatmapSetInfo>(item.ID);
+                realm.Write(r => item.CopyChangesToRealm(existing));
             }
-
-            Beatmap beatmap;
-            using (var stream = new LineBufferedReader(reader.GetStream(mapName)))
-                beatmap = Decoder.GetDecoder<Beatmap>(stream).Decode(stream);
-
-            return new BeatmapSetInfo
-            {
-                OnlineID = beatmap.BeatmapInfo.BeatmapSet?.OnlineID,
-                Metadata = beatmap.Metadata,
-                DateAdded = DateTimeOffset.UtcNow
-            };
         }
-
-        /// <summary>
-        /// Create all required <see cref="BeatmapInfo"/>s for the provided archive.
-        /// </summary>
-        private List<BeatmapInfo> createBeatmapDifficulties(List<BeatmapSetFileInfo> files)
-        {
-            var beatmapInfos = new List<BeatmapInfo>();
-
-            foreach (var file in files.Where(f => f.Filename.EndsWith(".osu", StringComparison.OrdinalIgnoreCase)))
-            {
-                using (var raw = Files.Store.GetStream(file.FileInfo.GetStoragePath()))
-                using (var ms = new MemoryStream()) // we need a memory stream so we can seek
-                using (var sr = new LineBufferedReader(ms))
-                {
-                    raw.CopyTo(ms);
-                    ms.Position = 0;
-
-                    var decoder = Decoder.GetDecoder<Beatmap>(sr);
-                    IBeatmap beatmap = decoder.Decode(sr);
-
-                    string hash = ms.ComputeSHA2Hash();
-
-                    if (beatmapInfos.Any(b => b.Hash == hash))
-                        continue;
-
-                    beatmap.BeatmapInfo.Path = file.Filename;
-                    beatmap.BeatmapInfo.Hash = hash;
-                    beatmap.BeatmapInfo.MD5Hash = ms.ComputeMD5Hash();
-
-                    var ruleset = rulesets.GetRuleset(beatmap.BeatmapInfo.RulesetID);
-                    beatmap.BeatmapInfo.Ruleset = ruleset;
-
-                    // TODO: this should be done in a better place once we actually need to dynamically update it.
-                    beatmap.BeatmapInfo.StarRating = ruleset?.CreateInstance().CreateDifficultyCalculator(new DummyConversionBeatmap(beatmap)).Calculate().StarRating ?? 0;
-                    beatmap.BeatmapInfo.Length = calculateLength(beatmap);
-                    beatmap.BeatmapInfo.BPM = 60000 / beatmap.GetMostCommonBeatLength();
-
-                    beatmapInfos.Add(beatmap.BeatmapInfo);
-                }
-            }
-
-            return beatmapInfos;
-        }
-
-        private double calculateLength(IBeatmap b)
-        {
-            if (!b.HitObjects.Any())
-                return 0;
-
-            var lastObject = b.HitObjects.Last();
-
-            //TODO: this isn't always correct (consider mania where a non-last object may last for longer than the last in the list).
-            double endTime = lastObject.GetEndTime();
-            double startTime = b.HitObjects.First().StartTime;
-
-            return endTime - startTime;
-        }
-
-        /// <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, null)
-            {
-                this.beatmap = beatmap;
-            }
-
-            protected override IBeatmap GetBeatmap() => beatmap;
-            protected override Texture GetBackground() => null;
-            protected override Track GetBeatmapTrack() => null;
-            protected internal override ISkin GetSkin() => null;
-            public override Stream GetStream(string storagePath) => null;
-        }
-    }
-
-    /// <summary>
-    /// The level of detail to include in database results.
-    /// </summary>
-    public enum IncludedDetails
-    {
-        /// <summary>
-        /// Only include beatmap difficulties and set level metadata.
-        /// </summary>
-        Minimal,
-
-        /// <summary>
-        /// Include all difficulties, rulesets, difficulty metadata but no files.
-        /// </summary>
-        AllButFiles,
-
-        /// <summary>
-        /// Include everything except ruleset. Used for cases where we aren't sure the ruleset is present but still want to consume the beatmap.
-        /// </summary>
-        AllButRuleset,
-
-        /// <summary>
-        /// Include everything.
-        /// </summary>
-        All
     }
 }
diff --git a/osu.Game/Beatmaps/BeatmapOnlineLookupQueue.cs b/osu.Game/Beatmaps/BeatmapOnlineLookupQueue.cs
index 76232c2932..a24b6b315a 100644
--- a/osu.Game/Beatmaps/BeatmapOnlineLookupQueue.cs
+++ b/osu.Game/Beatmaps/BeatmapOnlineLookupQueue.cs
@@ -2,6 +2,7 @@
 // See the LICENCE file in the repository root for full licence text.
 
 using System;
+using System.Diagnostics;
 using System.IO;
 using System.Linq;
 using System.Threading;
@@ -16,6 +17,7 @@ using osu.Framework.Threading;
 using osu.Game.Database;
 using osu.Game.Online.API;
 using osu.Game.Online.API.Requests;
+using osu.Game.Stores;
 using SharpCompress.Compressors;
 using SharpCompress.Compressors.BZip2;
 
@@ -52,6 +54,12 @@ namespace osu.Game.Beatmaps
                 prepareLocalCache();
         }
 
+        public void Update(BeatmapSetInfo beatmapSet)
+        {
+            foreach (var b in beatmapSet.Beatmaps)
+                lookup(beatmapSet, b);
+        }
+
         public Task UpdateAsync(BeatmapSetInfo beatmapSet, CancellationToken cancellationToken)
         {
             return Task.WhenAll(beatmapSet.Beatmaps.Select(b => UpdateAsync(beatmapSet, b, cancellationToken)).ToArray());
@@ -71,40 +79,39 @@ namespace osu.Game.Beatmaps
 
             var req = new GetBeatmapRequest(beatmapInfo);
 
-            req.Failure += fail;
-
             try
             {
                 // intentionally blocking to limit web request concurrency
                 api.Perform(req);
 
+                if (req.CompletionState == APIRequestCompletionState.Failed)
+                {
+                    logForModel(set, $"Online retrieval failed for {beatmapInfo}");
+                    beatmapInfo.OnlineID = -1;
+                    return;
+                }
+
                 var res = req.Response;
 
                 if (res != null)
                 {
                     beatmapInfo.Status = res.Status;
+
+                    Debug.Assert(beatmapInfo.BeatmapSet != null);
+
                     beatmapInfo.BeatmapSet.Status = res.BeatmapSet?.Status ?? BeatmapOnlineStatus.None;
                     beatmapInfo.BeatmapSet.OnlineID = res.OnlineBeatmapSetID;
                     beatmapInfo.OnlineID = res.OnlineID;
 
-                    if (beatmapInfo.Metadata != null)
-                        beatmapInfo.Metadata.AuthorID = res.AuthorID;
-
-                    if (beatmapInfo.BeatmapSet.Metadata != null)
-                        beatmapInfo.BeatmapSet.Metadata.AuthorID = res.AuthorID;
+                    beatmapInfo.Metadata.Author.OnlineID = res.AuthorID;
 
                     logForModel(set, $"Online retrieval mapped {beatmapInfo} to {res.OnlineBeatmapSetID} / {res.OnlineID}.");
                 }
             }
             catch (Exception e)
             {
-                fail(e);
-            }
-
-            void fail(Exception e)
-            {
-                beatmapInfo.OnlineID = null;
                 logForModel(set, $"Online retrieval failed for {beatmapInfo} ({e.Message})");
+                beatmapInfo.OnlineID = -1;
             }
         }
 
@@ -161,7 +168,7 @@ namespace osu.Game.Beatmaps
 
             if (string.IsNullOrEmpty(beatmapInfo.MD5Hash)
                 && string.IsNullOrEmpty(beatmapInfo.Path)
-                && beatmapInfo.OnlineID == null)
+                && beatmapInfo.OnlineID <= 0)
                 return false;
 
             try
@@ -175,7 +182,7 @@ namespace osu.Game.Beatmaps
                         cmd.CommandText = "SELECT beatmapset_id, beatmap_id, approved, user_id FROM osu_beatmaps WHERE checksum = @MD5Hash OR beatmap_id = @OnlineID OR filename = @Path";
 
                         cmd.Parameters.Add(new SqliteParameter("@MD5Hash", beatmapInfo.MD5Hash));
-                        cmd.Parameters.Add(new SqliteParameter("@OnlineID", beatmapInfo.OnlineID ?? (object)DBNull.Value));
+                        cmd.Parameters.Add(new SqliteParameter("@OnlineID", beatmapInfo.OnlineID));
                         cmd.Parameters.Add(new SqliteParameter("@Path", beatmapInfo.Path));
 
                         using (var reader = cmd.ExecuteReader())
@@ -185,15 +192,14 @@ namespace osu.Game.Beatmaps
                                 var status = (BeatmapOnlineStatus)reader.GetByte(2);
 
                                 beatmapInfo.Status = status;
+
+                                Debug.Assert(beatmapInfo.BeatmapSet != null);
+
                                 beatmapInfo.BeatmapSet.Status = status;
                                 beatmapInfo.BeatmapSet.OnlineID = reader.GetInt32(0);
                                 beatmapInfo.OnlineID = reader.GetInt32(1);
 
-                                if (beatmapInfo.Metadata != null)
-                                    beatmapInfo.Metadata.AuthorID = reader.GetInt32(3);
-
-                                if (beatmapInfo.BeatmapSet.Metadata != null)
-                                    beatmapInfo.BeatmapSet.Metadata.AuthorID = reader.GetInt32(3);
+                                beatmapInfo.Metadata.Author.OnlineID = reader.GetInt32(3);
 
                                 logForModel(set, $"Cached local retrieval for {beatmapInfo}.");
                                 return true;
@@ -211,7 +217,7 @@ namespace osu.Game.Beatmaps
         }
 
         private void logForModel(BeatmapSetInfo set, string message) =>
-            ArchiveModelManager<BeatmapSetInfo, BeatmapSetFileInfo>.LogForModel(set, $"[{nameof(BeatmapOnlineLookupQueue)}] {message}");
+            RealmArchiveModelImporter<BeatmapSetInfo>.LogForModel(set, $"[{nameof(BeatmapOnlineLookupQueue)}] {message}");
 
         public void Dispose()
         {
diff --git a/osu.Game/Beatmaps/BeatmapSetFileInfo.cs b/osu.Game/Beatmaps/BeatmapSetFileInfo.cs
index 29dcf4d6aa..3d41f59b3d 100644
--- a/osu.Game/Beatmaps/BeatmapSetFileInfo.cs
+++ b/osu.Game/Beatmaps/BeatmapSetFileInfo.cs
@@ -15,6 +15,8 @@ namespace osu.Game.Beatmaps
 
         public int BeatmapSetInfoID { get; set; }
 
+        public EFBeatmapSetInfo BeatmapSetInfo { get; set; }
+
         public int FileInfoID { get; set; }
 
         public FileInfo FileInfo { get; set; }
@@ -22,6 +24,6 @@ namespace osu.Game.Beatmaps
         [Required]
         public string Filename { get; set; }
 
-        public IFileInfo File => FileInfo;
+        IFileInfo INamedFileUsage.File => FileInfo;
     }
 }
diff --git a/osu.Game/Beatmaps/BeatmapSetInfo.cs b/osu.Game/Beatmaps/BeatmapSetInfo.cs
index a3a8f8555f..a934d1a2e3 100644
--- a/osu.Game/Beatmaps/BeatmapSetInfo.cs
+++ b/osu.Game/Beatmaps/BeatmapSetInfo.cs
@@ -3,102 +3,81 @@
 
 using System;
 using System.Collections.Generic;
-using System.ComponentModel.DataAnnotations.Schema;
 using System.Linq;
-using JetBrains.Annotations;
-using Newtonsoft.Json;
 using osu.Framework.Testing;
 using osu.Game.Database;
 using osu.Game.Extensions;
+using osu.Game.Models;
+using Realms;
+
+#nullable enable
 
 namespace osu.Game.Beatmaps
 {
     [ExcludeFromDynamicCompile]
-    public class BeatmapSetInfo : IHasPrimaryKey, IHasFiles<BeatmapSetFileInfo>, ISoftDelete, IEquatable<BeatmapSetInfo>, IBeatmapSetInfo
+    [MapTo("BeatmapSet")]
+    public class BeatmapSetInfo : RealmObject, IHasGuidPrimaryKey, IHasRealmFiles, ISoftDelete, IEquatable<BeatmapSetInfo>, IBeatmapSetInfo
     {
-        public int ID { get; set; }
+        [PrimaryKey]
+        public Guid ID { get; set; } = Guid.NewGuid();
 
-        public bool IsManaged => ID > 0;
-
-        private int? onlineID;
-
-        [Column("OnlineBeatmapSetID")]
-        public int? OnlineID
-        {
-            get => onlineID;
-            set => onlineID = value > 0 ? value : null;
-        }
+        [Indexed]
+        public int OnlineID { get; set; } = -1;
 
         public DateTimeOffset DateAdded { get; set; }
 
-        public BeatmapMetadata Metadata { get; set; }
+        public IBeatmapMetadataInfo Metadata => Beatmaps.FirstOrDefault()?.Metadata ?? new BeatmapMetadata();
 
-        [NotNull]
-        public List<BeatmapInfo> Beatmaps { get; } = new List<BeatmapInfo>();
+        public IList<BeatmapInfo> Beatmaps { get; } = null!;
 
-        public BeatmapOnlineStatus Status { get; set; } = BeatmapOnlineStatus.None;
+        public IList<RealmNamedFileUsage> Files { get; } = null!;
 
-        public List<BeatmapSetFileInfo> Files { get; } = new List<BeatmapSetFileInfo>();
+        [Ignored]
+        public BeatmapOnlineStatus Status
+        {
+            get => (BeatmapOnlineStatus)StatusInt;
+            set => StatusInt = (int)value;
+        }
 
-        /// <summary>
-        /// The maximum star difficulty of all beatmaps in this set.
-        /// </summary>
-        [JsonIgnore]
-        public double MaxStarDifficulty => Beatmaps.Count == 0 ? 0 : Beatmaps.Max(b => b.StarRating);
+        [MapTo(nameof(Status))]
+        public int StatusInt { get; set; } = (int)BeatmapOnlineStatus.None;
 
-        /// <summary>
-        /// The maximum playable length in milliseconds of all beatmaps in this set.
-        /// </summary>
-        [JsonIgnore]
-        public double MaxLength => Beatmaps.Count == 0 ? 0 : Beatmaps.Max(b => b.Length);
-
-        /// <summary>
-        /// The maximum BPM of all beatmaps in this set.
-        /// </summary>
-        [JsonIgnore]
-        public double MaxBPM => Beatmaps.Count == 0 ? 0 : Beatmaps.Max(b => b.BPM);
-
-        [NotMapped]
         public bool DeletePending { get; set; }
 
-        public string Hash { get; set; }
+        public string Hash { get; set; } = string.Empty;
+
+        /// <summary>
+        /// Whether deleting this beatmap set should be prohibited (due to it being a system requirement to be present).
+        /// </summary>
+        public bool Protected { get; set; }
+
+        public double MaxStarDifficulty => Beatmaps.Count == 0 ? 0 : Beatmaps.Max(b => b.StarRating);
+
+        public double MaxLength => Beatmaps.Count == 0 ? 0 : Beatmaps.Max(b => b.Length);
+
+        public double MaxBPM => Beatmaps.Count == 0 ? 0 : Beatmaps.Max(b => b.BPM);
 
         /// <summary>
         /// Returns the storage path for the file in this beatmapset with the given filename, if any exists, otherwise null.
         /// The path returned is relative to the user file storage.
         /// </summary>
         /// <param name="filename">The name of the file to get the storage path of.</param>
-        public string GetPathForFile(string filename) => Files.SingleOrDefault(f => string.Equals(f.Filename, filename, StringComparison.OrdinalIgnoreCase))?.FileInfo.GetStoragePath();
+        public string? GetPathForFile(string filename) => Files.SingleOrDefault(f => string.Equals(f.Filename, filename, StringComparison.OrdinalIgnoreCase))?.File.GetStoragePath();
 
-        public override string ToString() => Metadata?.ToString() ?? base.ToString();
-
-        public bool Protected { get; set; }
-
-        public bool Equals(BeatmapSetInfo other)
+        public bool Equals(BeatmapSetInfo? other)
         {
             if (ReferenceEquals(this, other)) return true;
             if (other == null) return false;
 
-            if (ID != 0 && other.ID != 0)
-                return ID == other.ID;
-
-            return false;
+            return ID == other.ID;
         }
 
-        public bool Equals(IBeatmapSetInfo other) => other is BeatmapSetInfo b && Equals(b);
+        public override string ToString() => Metadata.GetDisplayString();
 
-        #region Implementation of IHasOnlineID
+        public bool Equals(IBeatmapSetInfo? other) => other is BeatmapSetInfo b && Equals(b);
 
-        int IHasOnlineID<int>.OnlineID => OnlineID ?? -1;
-
-        #endregion
-
-        #region Implementation of IBeatmapSetInfo
-
-        IBeatmapMetadataInfo IBeatmapSetInfo.Metadata => Metadata ?? Beatmaps.FirstOrDefault()?.Metadata ?? new BeatmapMetadata();
         IEnumerable<IBeatmapInfo> IBeatmapSetInfo.Beatmaps => Beatmaps;
-        IEnumerable<INamedFileUsage> IHasNamedFiles.Files => Files;
 
-        #endregion
+        IEnumerable<INamedFileUsage> IHasNamedFiles.Files => Files;
     }
 }
diff --git a/osu.Game/Beatmaps/BeatmapStore.cs b/osu.Game/Beatmaps/BeatmapStore.cs
deleted file mode 100644
index 197581db88..0000000000
--- a/osu.Game/Beatmaps/BeatmapStore.cs
+++ /dev/null
@@ -1,117 +0,0 @@
-// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using Microsoft.EntityFrameworkCore;
-using osu.Game.Database;
-
-namespace osu.Game.Beatmaps
-{
-    /// <summary>
-    /// Handles the storage and retrieval of Beatmaps/BeatmapSets to the database backing
-    /// </summary>
-    public class BeatmapStore : MutableDatabaseBackedStoreWithFileIncludes<BeatmapSetInfo, BeatmapSetFileInfo>
-    {
-        public event Action<BeatmapInfo> BeatmapHidden;
-        public event Action<BeatmapInfo> BeatmapRestored;
-
-        public BeatmapStore(IDatabaseContextFactory factory)
-            : base(factory)
-        {
-        }
-
-        /// <summary>
-        /// Hide a <see cref="BeatmapInfo"/> in the database.
-        /// </summary>
-        /// <param name="beatmapInfo">The beatmap to hide.</param>
-        /// <returns>Whether the beatmap's <see cref="BeatmapInfo.Hidden"/> was changed.</returns>
-        public bool Hide(BeatmapInfo beatmapInfo)
-        {
-            using (ContextFactory.GetForWrite())
-            {
-                Refresh(ref beatmapInfo, Beatmaps);
-
-                if (beatmapInfo.Hidden) return false;
-
-                beatmapInfo.Hidden = true;
-            }
-
-            BeatmapHidden?.Invoke(beatmapInfo);
-            return true;
-        }
-
-        /// <summary>
-        /// Restore a previously hidden <see cref="BeatmapInfo"/>.
-        /// </summary>
-        /// <param name="beatmapInfo">The beatmap to restore.</param>
-        /// <returns>Whether the beatmap's <see cref="BeatmapInfo.Hidden"/> was changed.</returns>
-        public bool Restore(BeatmapInfo beatmapInfo)
-        {
-            using (ContextFactory.GetForWrite())
-            {
-                Refresh(ref beatmapInfo, Beatmaps);
-
-                if (!beatmapInfo.Hidden) return false;
-
-                beatmapInfo.Hidden = false;
-            }
-
-            BeatmapRestored?.Invoke(beatmapInfo);
-            return true;
-        }
-
-        protected override IQueryable<BeatmapSetInfo> AddIncludesForDeletion(IQueryable<BeatmapSetInfo> query) =>
-            base.AddIncludesForDeletion(query)
-                .Include(s => s.Metadata)
-                .Include(s => s.Beatmaps).ThenInclude(b => b.BaseDifficulty)
-                .Include(s => s.Beatmaps).ThenInclude(b => b.Metadata);
-
-        protected override IQueryable<BeatmapSetInfo> AddIncludesForConsumption(IQueryable<BeatmapSetInfo> query) =>
-            base.AddIncludesForConsumption(query)
-                .Include(s => s.Metadata)
-                .Include(s => s.Beatmaps).ThenInclude(s => s.Ruleset)
-                .Include(s => s.Beatmaps).ThenInclude(b => b.BaseDifficulty)
-                .Include(s => s.Beatmaps).ThenInclude(b => b.Metadata);
-
-        protected override void Purge(List<BeatmapSetInfo> items, OsuDbContext context)
-        {
-            // metadata is M-N so we can't rely on cascades
-            context.BeatmapMetadata.RemoveRange(items.Select(s => s.Metadata));
-            context.BeatmapMetadata.RemoveRange(items.SelectMany(s => s.Beatmaps.Select(b => b.Metadata).Where(m => m != null)));
-
-            // todo: we can probably make cascades work here with a FK in BeatmapDifficulty. just make to make it work correctly.
-            context.BeatmapDifficulty.RemoveRange(items.SelectMany(s => s.Beatmaps.Select(b => b.BaseDifficulty)));
-
-            base.Purge(items, context);
-        }
-
-        public IQueryable<BeatmapSetInfo> BeatmapSetsOverview => ContextFactory.Get().BeatmapSetInfo
-                                                                               .Include(s => s.Metadata)
-                                                                               .Include(s => s.Beatmaps)
-                                                                               .AsNoTracking();
-
-        public IQueryable<BeatmapSetInfo> BeatmapSetsWithoutRuleset => ContextFactory.Get().BeatmapSetInfo
-                                                                                     .Include(s => s.Metadata)
-                                                                                     .Include(s => s.Files).ThenInclude(f => f.FileInfo)
-                                                                                     .Include(s => s.Beatmaps).ThenInclude(b => b.BaseDifficulty)
-                                                                                     .Include(s => s.Beatmaps).ThenInclude(b => b.Metadata)
-                                                                                     .AsNoTracking();
-
-        public IQueryable<BeatmapSetInfo> BeatmapSetsWithoutFiles => ContextFactory.Get().BeatmapSetInfo
-                                                                                   .Include(s => s.Metadata)
-                                                                                   .Include(s => s.Beatmaps).ThenInclude(s => s.Ruleset)
-                                                                                   .Include(s => s.Beatmaps).ThenInclude(b => b.BaseDifficulty)
-                                                                                   .Include(s => s.Beatmaps).ThenInclude(b => b.Metadata)
-                                                                                   .AsNoTracking();
-
-        public IQueryable<BeatmapInfo> Beatmaps =>
-            ContextFactory.Get().BeatmapInfo
-                          .Include(b => b.BeatmapSet).ThenInclude(s => s.Metadata)
-                          .Include(b => b.BeatmapSet).ThenInclude(s => s.Files).ThenInclude(f => f.FileInfo)
-                          .Include(b => b.Metadata)
-                          .Include(b => b.Ruleset)
-                          .Include(b => b.BaseDifficulty);
-    }
-}
diff --git a/osu.Game/Beatmaps/DummyWorkingBeatmap.cs b/osu.Game/Beatmaps/DummyWorkingBeatmap.cs
index 9ea8517764..6e879d09d5 100644
--- a/osu.Game/Beatmaps/DummyWorkingBeatmap.cs
+++ b/osu.Game/Beatmaps/DummyWorkingBeatmap.cs
@@ -32,7 +32,7 @@ namespace osu.Game.Beatmaps
                     Title = "no beatmaps available!"
                 },
                 BeatmapSet = new BeatmapSetInfo(),
-                BaseDifficulty = new BeatmapDifficulty
+                Difficulty = new BeatmapDifficulty
                 {
                     DrainRate = 0,
                     CircleSize = 0,
diff --git a/osu.Game/Beatmaps/EFBeatmapDifficulty.cs b/osu.Game/Beatmaps/EFBeatmapDifficulty.cs
new file mode 100644
index 0000000000..38371d3b38
--- /dev/null
+++ b/osu.Game/Beatmaps/EFBeatmapDifficulty.cs
@@ -0,0 +1,78 @@
+// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.ComponentModel.DataAnnotations.Schema;
+using osu.Game.Database;
+
+namespace osu.Game.Beatmaps
+{
+    [Table(@"BeatmapDifficulty")]
+    public class EFBeatmapDifficulty : IHasPrimaryKey, IBeatmapDifficultyInfo
+    {
+        /// <summary>
+        /// The default value used for all difficulty settings except <see cref="SliderMultiplier"/> and <see cref="SliderTickRate"/>.
+        /// </summary>
+        public const float DEFAULT_DIFFICULTY = 5;
+
+        public int ID { get; set; }
+
+        public bool IsManaged => ID > 0;
+
+        public float DrainRate { get; set; } = DEFAULT_DIFFICULTY;
+        public float CircleSize { get; set; } = DEFAULT_DIFFICULTY;
+        public float OverallDifficulty { get; set; } = DEFAULT_DIFFICULTY;
+
+        private float? approachRate;
+
+        public EFBeatmapDifficulty()
+        {
+        }
+
+        public EFBeatmapDifficulty(IBeatmapDifficultyInfo source)
+        {
+            CopyFrom(source);
+        }
+
+        public float ApproachRate
+        {
+            get => approachRate ?? OverallDifficulty;
+            set => approachRate = value;
+        }
+
+        public double SliderMultiplier { get; set; } = 1;
+        public double SliderTickRate { get; set; } = 1;
+
+        /// <summary>
+        /// Returns a shallow-clone of this <see cref="EFBeatmapDifficulty"/>.
+        /// </summary>
+        public EFBeatmapDifficulty Clone()
+        {
+            var diff = (EFBeatmapDifficulty)Activator.CreateInstance(GetType());
+            CopyTo(diff);
+            return diff;
+        }
+
+        public virtual void CopyFrom(IBeatmapDifficultyInfo other)
+        {
+            ApproachRate = other.ApproachRate;
+            DrainRate = other.DrainRate;
+            CircleSize = other.CircleSize;
+            OverallDifficulty = other.OverallDifficulty;
+
+            SliderMultiplier = other.SliderMultiplier;
+            SliderTickRate = other.SliderTickRate;
+        }
+
+        public virtual void CopyTo(EFBeatmapDifficulty other)
+        {
+            other.ApproachRate = ApproachRate;
+            other.DrainRate = DrainRate;
+            other.CircleSize = CircleSize;
+            other.OverallDifficulty = OverallDifficulty;
+
+            other.SliderMultiplier = SliderMultiplier;
+            other.SliderTickRate = SliderTickRate;
+        }
+    }
+}
diff --git a/osu.Game/Beatmaps/EFBeatmapInfo.cs b/osu.Game/Beatmaps/EFBeatmapInfo.cs
new file mode 100644
index 0000000000..8daeaa7030
--- /dev/null
+++ b/osu.Game/Beatmaps/EFBeatmapInfo.cs
@@ -0,0 +1,184 @@
+// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+using Newtonsoft.Json;
+using osu.Framework.Testing;
+using osu.Game.Database;
+using osu.Game.Online.API.Requests.Responses;
+using osu.Game.Rulesets;
+using osu.Game.Scoring;
+
+namespace osu.Game.Beatmaps
+{
+    [ExcludeFromDynamicCompile]
+    [Serializable]
+    [Table(@"BeatmapInfo")]
+    public class EFBeatmapInfo : IEquatable<EFBeatmapInfo>, IHasPrimaryKey, IBeatmapInfo
+    {
+        public int ID { get; set; }
+
+        public bool IsManaged => ID > 0;
+
+        public int BeatmapVersion;
+
+        private int? onlineID;
+
+        [JsonProperty("id")]
+        [Column("OnlineBeatmapID")]
+        public int? OnlineID
+        {
+            get => onlineID;
+            set => onlineID = value > 0 ? value : null;
+        }
+
+        [JsonIgnore]
+        public int BeatmapSetInfoID { get; set; }
+
+        public BeatmapOnlineStatus Status { get; set; } = BeatmapOnlineStatus.None;
+
+        [Required]
+        public EFBeatmapSetInfo BeatmapSetInfo { get; set; }
+
+        public EFBeatmapMetadata Metadata { get; set; }
+
+        [JsonIgnore]
+        public int BaseDifficultyID { get; set; }
+
+        public EFBeatmapDifficulty BaseDifficulty { get; set; }
+
+        [NotMapped]
+        public APIBeatmap OnlineInfo { get; set; }
+
+        [NotMapped]
+        public int? MaxCombo { get; set; }
+
+        /// <summary>
+        /// The playable length in milliseconds of this beatmap.
+        /// </summary>
+        public double Length { get; set; }
+
+        /// <summary>
+        /// The most common BPM of this beatmap.
+        /// </summary>
+        public double BPM { get; set; }
+
+        public string Path { get; set; }
+
+        [JsonProperty("file_sha2")]
+        public string Hash { get; set; }
+
+        [JsonIgnore]
+        public bool Hidden { get; set; }
+
+        /// <summary>
+        /// MD5 is kept for legacy support (matching against replays, osu-web-10 etc.).
+        /// </summary>
+        [JsonProperty("file_md5")]
+        public string MD5Hash { get; set; }
+
+        // General
+        public double AudioLeadIn { get; set; }
+        public float StackLeniency { get; set; } = 0.7f;
+        public bool SpecialStyle { get; set; }
+
+        [Column("RulesetID")]
+        public int RulesetInfoID { get; set; }
+
+        public EFRulesetInfo RulesetInfo { get; set; }
+
+        public bool LetterboxInBreaks { get; set; }
+        public bool WidescreenStoryboard { get; set; }
+        public bool EpilepsyWarning { get; set; }
+
+        /// <summary>
+        /// Whether or not sound samples should change rate when playing with speed-changing mods.
+        /// TODO: only read/write supported for now, requires implementation in gameplay.
+        /// </summary>
+        public bool SamplesMatchPlaybackRate { get; set; }
+
+        public CountdownType Countdown { get; set; } = CountdownType.Normal;
+
+        /// <summary>
+        /// The number of beats to move the countdown backwards (compared to its default location).
+        /// </summary>
+        public int CountdownOffset { get; set; }
+
+        [NotMapped]
+        public int[] Bookmarks { get; set; } = Array.Empty<int>();
+
+        public double DistanceSpacing { get; set; }
+        public int BeatDivisor { get; set; }
+        public int GridSize { get; set; }
+        public double TimelineZoom { get; set; }
+
+        // Metadata
+        [Column("Version")]
+        public string DifficultyName { get; set; }
+
+        [JsonProperty("difficulty_rating")]
+        [Column("StarDifficulty")]
+        public double StarRating { get; set; }
+
+        /// <summary>
+        /// Currently only populated for beatmap deletion. Use <see cref="ScoreManager"/> to query scores.
+        /// </summary>
+        public List<EFScoreInfo> Scores { get; set; }
+
+        [JsonIgnore]
+        public DifficultyRating DifficultyRating => BeatmapDifficultyCache.GetDifficultyRating(StarRating);
+
+        public override string ToString() => this.GetDisplayTitle();
+
+        public bool Equals(EFBeatmapInfo other)
+        {
+            if (ReferenceEquals(this, other)) return true;
+            if (other == null) return false;
+
+            if (ID != 0 && other.ID != 0)
+                return ID == other.ID;
+
+            return false;
+        }
+
+        public bool Equals(IBeatmapInfo other) => other is EFBeatmapInfo b && Equals(b);
+
+        public bool AudioEquals(EFBeatmapInfo other) => other != null && BeatmapSetInfo != null && other.BeatmapSetInfo != null &&
+                                                        BeatmapSetInfo.Hash == other.BeatmapSetInfo.Hash &&
+                                                        (Metadata ?? BeatmapSetInfo.Metadata).AudioFile == (other.Metadata ?? other.BeatmapSetInfo.Metadata).AudioFile;
+
+        public bool BackgroundEquals(EFBeatmapInfo other) => other != null && BeatmapSetInfo != null && other.BeatmapSetInfo != null &&
+                                                             BeatmapSetInfo.Hash == other.BeatmapSetInfo.Hash &&
+                                                             (Metadata ?? BeatmapSetInfo.Metadata).BackgroundFile == (other.Metadata ?? other.BeatmapSetInfo.Metadata).BackgroundFile;
+
+        /// <summary>
+        /// Returns a shallow-clone of this <see cref="EFBeatmapInfo"/>.
+        /// </summary>
+        public EFBeatmapInfo Clone() => (EFBeatmapInfo)MemberwiseClone();
+
+        #region Implementation of IHasOnlineID
+
+        int IHasOnlineID<int>.OnlineID => OnlineID ?? -1;
+
+        #endregion
+
+        #region Implementation of IBeatmapInfo
+
+        [JsonIgnore]
+        IBeatmapMetadataInfo IBeatmapInfo.Metadata => Metadata ?? BeatmapSetInfo?.Metadata ?? new EFBeatmapMetadata();
+
+        [JsonIgnore]
+        IBeatmapDifficultyInfo IBeatmapInfo.Difficulty => BaseDifficulty;
+
+        [JsonIgnore]
+        IBeatmapSetInfo IBeatmapInfo.BeatmapSet => BeatmapSetInfo;
+
+        [JsonIgnore]
+        IRulesetInfo IBeatmapInfo.Ruleset => RulesetInfo;
+
+        #endregion
+    }
+}
diff --git a/osu.Game/Beatmaps/EFBeatmapMetadata.cs b/osu.Game/Beatmaps/EFBeatmapMetadata.cs
new file mode 100644
index 0000000000..7c27863a7f
--- /dev/null
+++ b/osu.Game/Beatmaps/EFBeatmapMetadata.cs
@@ -0,0 +1,91 @@
+// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations.Schema;
+using Newtonsoft.Json;
+using osu.Framework.Testing;
+using osu.Game.Database;
+using osu.Game.Online.API.Requests.Responses;
+using osu.Game.Users;
+
+#nullable enable
+
+namespace osu.Game.Beatmaps
+{
+    [ExcludeFromDynamicCompile]
+    [Serializable]
+    [Table(@"BeatmapMetadata")]
+    public class EFBeatmapMetadata : IEquatable<EFBeatmapMetadata>, IHasPrimaryKey, IBeatmapMetadataInfo
+    {
+        public int ID { get; set; }
+
+        public bool IsManaged => ID > 0;
+
+        public string Title { get; set; } = string.Empty;
+
+        [JsonProperty("title_unicode")]
+        public string TitleUnicode { get; set; } = string.Empty;
+
+        public string Artist { get; set; } = string.Empty;
+
+        [JsonProperty("artist_unicode")]
+        public string ArtistUnicode { get; set; } = string.Empty;
+
+        [JsonIgnore]
+        public List<EFBeatmapInfo> Beatmaps { get; set; } = new List<EFBeatmapInfo>();
+
+        [JsonIgnore]
+        public List<EFBeatmapSetInfo> BeatmapSets { get; set; } = new List<EFBeatmapSetInfo>();
+
+        /// <summary>
+        /// The author of the beatmaps in this set.
+        /// </summary>
+        [JsonIgnore]
+        public APIUser Author = new APIUser();
+
+        /// <summary>
+        /// Helper property to deserialize a username to <see cref="APIUser"/>.
+        /// </summary>
+        [JsonProperty(@"user_id")]
+        [Column("AuthorID")]
+        public int AuthorID
+        {
+            get => Author.Id; // This should not be used, but is required to make EF work correctly.
+            set => Author.Id = value;
+        }
+
+        /// <summary>
+        /// Helper property to deserialize a username to <see cref="APIUser"/>.
+        /// </summary>
+        [JsonProperty(@"creator")]
+        [Column("Author")]
+        public string AuthorString
+        {
+            get => Author.Username; // This should not be used, but is required to make EF work correctly.
+            set => Author.Username = value;
+        }
+
+        public string Source { get; set; } = string.Empty;
+
+        [JsonProperty(@"tags")]
+        public string Tags { get; set; } = string.Empty;
+
+        /// <summary>
+        /// The time in milliseconds to begin playing the track for preview purposes.
+        /// If -1, the track should begin playing at 40% of its length.
+        /// </summary>
+        public int PreviewTime { get; set; } = -1;
+
+        public string AudioFile { get; set; } = string.Empty;
+
+        public string BackgroundFile { get; set; } = string.Empty;
+
+        public bool Equals(EFBeatmapMetadata other) => ((IBeatmapMetadataInfo)this).Equals(other);
+
+        public override string ToString() => this.GetDisplayTitle();
+
+        IUser IBeatmapMetadataInfo.Author => Author;
+    }
+}
diff --git a/osu.Game/Beatmaps/EFBeatmapSetInfo.cs b/osu.Game/Beatmaps/EFBeatmapSetInfo.cs
new file mode 100644
index 0000000000..12235abce0
--- /dev/null
+++ b/osu.Game/Beatmaps/EFBeatmapSetInfo.cs
@@ -0,0 +1,106 @@
+// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations.Schema;
+using System.Linq;
+using JetBrains.Annotations;
+using Newtonsoft.Json;
+using osu.Framework.Testing;
+using osu.Game.Database;
+using osu.Game.Extensions;
+
+namespace osu.Game.Beatmaps
+{
+    [ExcludeFromDynamicCompile]
+    [Serializable]
+    [Table(@"BeatmapSetInfo")]
+    public class EFBeatmapSetInfo : IHasPrimaryKey, IHasFiles<BeatmapSetFileInfo>, ISoftDelete, IEquatable<EFBeatmapSetInfo>, IBeatmapSetInfo
+    {
+        public int ID { get; set; }
+
+        public bool IsManaged => ID > 0;
+
+        private int? onlineID;
+
+        [Column("OnlineBeatmapSetID")]
+        public int? OnlineID
+        {
+            get => onlineID;
+            set => onlineID = value > 0 ? value : null;
+        }
+
+        public DateTimeOffset DateAdded { get; set; }
+
+        public EFBeatmapMetadata Metadata { get; set; }
+
+        [NotNull]
+        public List<EFBeatmapInfo> Beatmaps { get; } = new List<EFBeatmapInfo>();
+
+        public BeatmapOnlineStatus Status { get; set; } = BeatmapOnlineStatus.None;
+
+        public List<BeatmapSetFileInfo> Files { get; } = new List<BeatmapSetFileInfo>();
+
+        /// <summary>
+        /// The maximum star difficulty of all beatmaps in this set.
+        /// </summary>
+        [JsonIgnore]
+        public double MaxStarDifficulty => Beatmaps.Count == 0 ? 0 : Beatmaps.Max(b => b.StarRating);
+
+        /// <summary>
+        /// The maximum playable length in milliseconds of all beatmaps in this set.
+        /// </summary>
+        [JsonIgnore]
+        public double MaxLength => Beatmaps.Count == 0 ? 0 : Beatmaps.Max(b => b.Length);
+
+        /// <summary>
+        /// The maximum BPM of all beatmaps in this set.
+        /// </summary>
+        [JsonIgnore]
+        public double MaxBPM => Beatmaps.Count == 0 ? 0 : Beatmaps.Max(b => b.BPM);
+
+        [NotMapped]
+        public bool DeletePending { get; set; }
+
+        public string Hash { get; set; }
+
+        /// <summary>
+        /// Returns the storage path for the file in this beatmapset with the given filename, if any exists, otherwise null.
+        /// The path returned is relative to the user file storage.
+        /// </summary>
+        /// <param name="filename">The name of the file to get the storage path of.</param>
+        public string GetPathForFile(string filename) => Files.SingleOrDefault(f => string.Equals(f.Filename, filename, StringComparison.OrdinalIgnoreCase))?.FileInfo.GetStoragePath();
+
+        public override string ToString() => Metadata?.ToString() ?? base.ToString();
+
+        public bool Protected { get; set; }
+
+        public bool Equals(EFBeatmapSetInfo other)
+        {
+            if (ReferenceEquals(this, other)) return true;
+            if (other == null) return false;
+
+            if (ID != 0 && other.ID != 0)
+                return ID == other.ID;
+
+            return false;
+        }
+
+        public bool Equals(IBeatmapSetInfo other) => other is EFBeatmapSetInfo b && Equals(b);
+
+        #region Implementation of IHasOnlineID
+
+        int IHasOnlineID<int>.OnlineID => OnlineID ?? -1;
+
+        #endregion
+
+        #region Implementation of IBeatmapSetInfo
+
+        IBeatmapMetadataInfo IBeatmapSetInfo.Metadata => Metadata ?? Beatmaps.FirstOrDefault()?.Metadata ?? new EFBeatmapMetadata();
+        IEnumerable<IBeatmapInfo> IBeatmapSetInfo.Beatmaps => Beatmaps;
+        IEnumerable<INamedFileUsage> IHasNamedFiles.Files => Files;
+
+        #endregion
+    }
+}
diff --git a/osu.Game/Beatmaps/FlatFileWorkingBeatmap.cs b/osu.Game/Beatmaps/FlatFileWorkingBeatmap.cs
index 8c915e2872..dc8201a402 100644
--- a/osu.Game/Beatmaps/FlatFileWorkingBeatmap.cs
+++ b/osu.Game/Beatmaps/FlatFileWorkingBeatmap.cs
@@ -33,7 +33,7 @@ namespace osu.Game.Beatmaps
             beatmap.BeatmapInfo.Ruleset = rulesetProvider(beatmap.BeatmapInfo.RulesetID).RulesetInfo;
 
             if (beatmapId.HasValue)
-                beatmap.BeatmapInfo.OnlineID = beatmapId;
+                beatmap.BeatmapInfo.OnlineID = beatmapId.Value;
         }
 
         private static Beatmap readFromFile(string filename)
diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs
index e5db9d045a..893eb8ab78 100644
--- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs
+++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs
@@ -251,7 +251,7 @@ namespace osu.Game.Beatmaps.Formats
                     break;
 
                 case @"Creator":
-                    metadata.AuthorString = pair.Value;
+                    metadata.Author.Username = pair.Value;
                     break;
 
                 case @"Version":
diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs
index 49853418d6..ebdc882d2f 100644
--- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs
+++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs
@@ -133,8 +133,8 @@ namespace osu.Game.Beatmaps.Formats
             writer.WriteLine(FormattableString.Invariant($"Version: {beatmap.BeatmapInfo.DifficultyName}"));
             if (!string.IsNullOrEmpty(beatmap.Metadata.Source)) writer.WriteLine(FormattableString.Invariant($"Source: {beatmap.Metadata.Source}"));
             if (!string.IsNullOrEmpty(beatmap.Metadata.Tags)) writer.WriteLine(FormattableString.Invariant($"Tags: {beatmap.Metadata.Tags}"));
-            if (beatmap.BeatmapInfo.OnlineID != null) writer.WriteLine(FormattableString.Invariant($"BeatmapID: {beatmap.BeatmapInfo.OnlineID}"));
-            if (beatmap.BeatmapInfo.BeatmapSet?.OnlineID != null) writer.WriteLine(FormattableString.Invariant($"BeatmapSetID: {beatmap.BeatmapInfo.BeatmapSet.OnlineID}"));
+            if (beatmap.BeatmapInfo.OnlineID > 0) writer.WriteLine(FormattableString.Invariant($"BeatmapID: {beatmap.BeatmapInfo.OnlineID}"));
+            if (beatmap.BeatmapInfo.BeatmapSet?.OnlineID > 0) writer.WriteLine(FormattableString.Invariant($"BeatmapSetID: {beatmap.BeatmapInfo.BeatmapSet.OnlineID}"));
         }
 
         private void handleDifficulty(TextWriter writer)
diff --git a/osu.Game/Beatmaps/IBeatmapMetadataInfo.cs b/osu.Game/Beatmaps/IBeatmapMetadataInfo.cs
index 968ad14928..61adc0ac34 100644
--- a/osu.Game/Beatmaps/IBeatmapMetadataInfo.cs
+++ b/osu.Game/Beatmaps/IBeatmapMetadataInfo.cs
@@ -73,7 +73,7 @@ namespace osu.Game.Beatmaps
                    && TitleUnicode == other.TitleUnicode
                    && Artist == other.Artist
                    && ArtistUnicode == other.ArtistUnicode
-                   && Author == other.Author
+                   && Author.Equals(other.Author)
                    && Source == other.Source
                    && Tags == other.Tags
                    && PreviewTime == other.PreviewTime
diff --git a/osu.Game/Beatmaps/IBeatmapSetOnlineInfo.cs b/osu.Game/Beatmaps/IBeatmapSetOnlineInfo.cs
index 0510770d5b..33d8929008 100644
--- a/osu.Game/Beatmaps/IBeatmapSetOnlineInfo.cs
+++ b/osu.Game/Beatmaps/IBeatmapSetOnlineInfo.cs
@@ -28,7 +28,7 @@ namespace osu.Game.Beatmaps
         DateTimeOffset? LastUpdated { get; }
 
         /// <summary>
-        /// The status of this beatmap set.
+        /// The "ranked" status of this beatmap set.
         /// </summary>
         BeatmapOnlineStatus Status { get; }
 
diff --git a/osu.Game/Beatmaps/WorkingBeatmap.cs b/osu.Game/Beatmaps/WorkingBeatmap.cs
index 8289b32d31..397d47c389 100644
--- a/osu.Game/Beatmaps/WorkingBeatmap.cs
+++ b/osu.Game/Beatmaps/WorkingBeatmap.cs
@@ -31,7 +31,7 @@ namespace osu.Game.Beatmaps
         public readonly BeatmapSetInfo BeatmapSetInfo;
 
         // TODO: remove once the fallback lookup is not required (and access via `working.BeatmapInfo.Metadata` directly).
-        public BeatmapMetadata Metadata => BeatmapInfo.Metadata ?? BeatmapSetInfo?.Metadata ?? new BeatmapMetadata();
+        public BeatmapMetadata Metadata => BeatmapInfo.Metadata;
 
         public Waveform Waveform => waveform.Value;
 
@@ -57,7 +57,7 @@ namespace osu.Game.Beatmaps
             this.audioManager = audioManager;
 
             BeatmapInfo = beatmapInfo;
-            BeatmapSetInfo = beatmapInfo.BeatmapSet;
+            BeatmapSetInfo = beatmapInfo.BeatmapSet ?? new BeatmapSetInfo();
 
             waveform = new Lazy<Waveform>(GetWaveform);
             storyboard = new Lazy<Storyboard>(GetStoryboard);
diff --git a/osu.Game/Beatmaps/WorkingBeatmapCache.cs b/osu.Game/Beatmaps/WorkingBeatmapCache.cs
index 514551e184..6947752c47 100644
--- a/osu.Game/Beatmaps/WorkingBeatmapCache.cs
+++ b/osu.Game/Beatmaps/WorkingBeatmapCache.cs
@@ -31,8 +31,6 @@ namespace osu.Game.Beatmaps
         /// </summary>
         public readonly WorkingBeatmap DefaultBeatmap;
 
-        public BeatmapModelManager BeatmapManager { private get; set; }
-
         private readonly AudioManager audioManager;
         private readonly IResourceStore<byte[]> resources;
         private readonly LargeTextureStore largeTextureStore;
@@ -76,13 +74,6 @@ namespace osu.Game.Beatmaps
 
         public virtual WorkingBeatmap GetWorkingBeatmap(BeatmapInfo beatmapInfo)
         {
-            // if there are no files, presume the full beatmap info has not yet been fetched from the database.
-            if (beatmapInfo?.BeatmapSet?.Files.Count == 0)
-            {
-                int lookupId = beatmapInfo.ID;
-                beatmapInfo = BeatmapManager.QueryBeatmap(b => b.ID == lookupId);
-            }
-
             if (beatmapInfo?.BeatmapSet == null)
                 return DefaultBeatmap;
 
@@ -93,12 +84,12 @@ namespace osu.Game.Beatmaps
                 if (working != null)
                     return working;
 
-                beatmapInfo.Metadata ??= beatmapInfo.BeatmapSet.Metadata;
+                beatmapInfo = beatmapInfo.Detach();
 
                 workingCache.Add(working = new BeatmapManagerWorkingBeatmap(beatmapInfo, this));
 
                 // best effort; may be higher than expected.
-                GlobalStatistics.Get<int>(nameof(Beatmaps), $"Cached {nameof(WorkingBeatmap)}s").Value = workingCache.Count();
+                GlobalStatistics.Get<int>("Beatmaps", $"Cached {nameof(WorkingBeatmap)}s").Value = workingCache.Count();
 
                 return working;
             }
@@ -198,6 +189,9 @@ namespace osu.Game.Beatmaps
             {
                 Storyboard storyboard;
 
+                if (BeatmapInfo.Path == null)
+                    return new Storyboard();
+
                 try
                 {
                     using (var stream = new LineBufferedReader(GetStream(BeatmapSetInfo.GetPathForFile(BeatmapInfo.Path))))
diff --git a/osu.Game/Collections/CollectionManager.cs b/osu.Game/Collections/CollectionManager.cs
index c4f991094c..d230e649f7 100644
--- a/osu.Game/Collections/CollectionManager.cs
+++ b/osu.Game/Collections/CollectionManager.cs
@@ -209,7 +209,7 @@ namespace osu.Game.Collections
 
                             string checksum = sr.ReadString();
 
-                            var beatmap = beatmaps.QueryBeatmap(b => b.MD5Hash == checksum);
+                            var beatmap = beatmaps.QueryBeatmap(b => b.MD5Hash == checksum)?.Detach();
                             if (beatmap != null)
                                 collection.Beatmaps.Add(beatmap);
                         }
diff --git a/osu.Game/Collections/DrawableCollectionListItem.cs b/osu.Game/Collections/DrawableCollectionListItem.cs
index 909595bd1c..c4cb040b52 100644
--- a/osu.Game/Collections/DrawableCollectionListItem.cs
+++ b/osu.Game/Collections/DrawableCollectionListItem.cs
@@ -80,7 +80,7 @@ namespace osu.Game.Collections
             }
 
             [BackgroundDependencyLoader]
-            private void load(OsuColour colours)
+            private void load()
             {
                 Children = new Drawable[]
                 {
diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs
deleted file mode 100644
index 9c26451d40..0000000000
--- a/osu.Game/Database/ArchiveModelManager.cs
+++ /dev/null
@@ -1,838 +0,0 @@
-// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Linq;
-using System.Threading;
-using System.Threading.Tasks;
-using Humanizer;
-using JetBrains.Annotations;
-using Microsoft.EntityFrameworkCore;
-using osu.Framework.Extensions;
-using osu.Framework.Extensions.IEnumerableExtensions;
-using osu.Framework.Logging;
-using osu.Framework.Platform;
-using osu.Framework.Threading;
-using osu.Game.Extensions;
-using osu.Game.IO;
-using osu.Game.IO.Archives;
-using osu.Game.IPC;
-using osu.Game.Overlays.Notifications;
-
-namespace osu.Game.Database
-{
-    /// <summary>
-    /// Encapsulates a model store class to give it import functionality.
-    /// Adds cross-functionality with <see cref="FileStore"/> to give access to the central file store for the provided model.
-    /// </summary>
-    /// <typeparam name="TModel">The model type.</typeparam>
-    /// <typeparam name="TFileModel">The associated file join type.</typeparam>
-    public abstract class ArchiveModelManager<TModel, TFileModel> : IModelImporter<TModel>, IModelManager<TModel>, IModelFileManager<TModel, TFileModel>
-        where TModel : class, IHasFiles<TFileModel>, IHasPrimaryKey, ISoftDelete
-        where TFileModel : class, INamedFileInfo, IHasPrimaryKey, new()
-    {
-        private const int import_queue_request_concurrency = 1;
-
-        /// <summary>
-        /// The size of a batch import operation before considering it a lower priority operation.
-        /// </summary>
-        private const int low_priority_import_batch_size = 1;
-
-        /// <summary>
-        /// A singleton scheduler shared by all <see cref="ArchiveModelManager{TModel,TFileModel}"/>.
-        /// </summary>
-        /// <remarks>
-        /// This scheduler generally performs IO and CPU intensive work so concurrency is limited harshly.
-        /// It is mainly being used as a queue mechanism for large imports.
-        /// </remarks>
-        private static readonly ThreadedTaskScheduler import_scheduler = new ThreadedTaskScheduler(import_queue_request_concurrency, nameof(ArchiveModelManager<TModel, TFileModel>));
-
-        /// <summary>
-        /// A second scheduler for lower priority imports.
-        /// For simplicity, these will just run in parallel with normal priority imports, but a future refactor would see this implemented via a custom scheduler/queue.
-        /// See https://gist.github.com/peppy/f0e118a14751fc832ca30dd48ba3876b for an incomplete version of this.
-        /// </summary>
-        private static readonly ThreadedTaskScheduler import_scheduler_low_priority = new ThreadedTaskScheduler(import_queue_request_concurrency, nameof(ArchiveModelManager<TModel, TFileModel>));
-
-        public Action<Notification> PostNotification { protected get; set; }
-
-        /// <summary>
-        /// Fired when a new or updated <typeparamref name="TModel"/> becomes available in the database.
-        /// This is not guaranteed to run on the update thread.
-        /// </summary>
-        public event Action<TModel> ItemUpdated;
-
-        /// <summary>
-        /// Fired when a <typeparamref name="TModel"/> is removed from the database.
-        /// This is not guaranteed to run on the update thread.
-        /// </summary>
-        public event Action<TModel> ItemRemoved;
-
-        public virtual IEnumerable<string> HandledExtensions => new[] { @".zip" };
-
-        protected readonly FileStore Files;
-
-        protected readonly IDatabaseContextFactory ContextFactory;
-
-        protected readonly MutableDatabaseBackedStore<TModel> ModelStore;
-
-        // ReSharper disable once NotAccessedField.Local (we should keep a reference to this so it is not finalised)
-        private ArchiveImportIPCChannel ipc;
-
-        protected ArchiveModelManager(Storage storage, IDatabaseContextFactory contextFactory, MutableDatabaseBackedStoreWithFileIncludes<TModel, TFileModel> modelStore, IIpcHost importHost = null)
-        {
-            ContextFactory = contextFactory;
-
-            ModelStore = modelStore;
-            ModelStore.ItemUpdated += item => handleEvent(() => ItemUpdated?.Invoke(item));
-            ModelStore.ItemRemoved += item => handleEvent(() => ItemRemoved?.Invoke(item));
-
-            Files = new FileStore(contextFactory, storage);
-
-            if (importHost != null)
-                ipc = new ArchiveImportIPCChannel(importHost, this);
-
-            ModelStore.Cleanup();
-        }
-
-        /// <summary>
-        /// Import one or more <typeparamref name="TModel"/> items from filesystem <paramref name="paths"/>.
-        /// </summary>
-        /// <remarks>
-        /// This will be treated as a low priority import if more than one path is specified; use <see cref="Import(ImportTask[])"/> to always import at standard priority.
-        /// This will post notifications tracking progress.
-        /// </remarks>
-        /// <param name="paths">One or more archive locations on disk.</param>
-        public Task Import(params string[] paths)
-        {
-            var notification = new ImportProgressNotification();
-
-            PostNotification?.Invoke(notification);
-
-            return Import(notification, paths.Select(p => new ImportTask(p)).ToArray());
-        }
-
-        public Task Import(params ImportTask[] tasks)
-        {
-            var notification = new ImportProgressNotification();
-
-            PostNotification?.Invoke(notification);
-
-            return Import(notification, tasks);
-        }
-
-        public async Task<IEnumerable<ILive<TModel>>> Import(ProgressNotification notification, params ImportTask[] tasks)
-        {
-            if (tasks.Length == 0)
-            {
-                notification.CompletionText = $"No {HumanisedModelName}s were found to import!";
-                notification.State = ProgressNotificationState.Completed;
-                return Enumerable.Empty<ILive<TModel>>();
-            }
-
-            notification.Progress = 0;
-            notification.Text = $"{HumanisedModelName.Humanize(LetterCasing.Title)} import is initialising...";
-
-            int current = 0;
-
-            var imported = new List<ILive<TModel>>();
-
-            bool isLowPriorityImport = tasks.Length > low_priority_import_batch_size;
-
-            try
-            {
-                await Task.WhenAll(tasks.Select(async task =>
-                {
-                    notification.CancellationToken.ThrowIfCancellationRequested();
-
-                    try
-                    {
-                        var model = await Import(task, isLowPriorityImport, notification.CancellationToken).ConfigureAwait(false);
-
-                        lock (imported)
-                        {
-                            if (model != null)
-                                imported.Add(model);
-                            current++;
-
-                            notification.Text = $"Imported {current} of {tasks.Length} {HumanisedModelName}s";
-                            notification.Progress = (float)current / tasks.Length;
-                        }
-                    }
-                    catch (TaskCanceledException)
-                    {
-                        throw;
-                    }
-                    catch (Exception e)
-                    {
-                        Logger.Error(e, $@"Could not import ({task})", LoggingTarget.Database);
-                    }
-                })).ConfigureAwait(false);
-            }
-            catch (OperationCanceledException)
-            {
-                if (imported.Count == 0)
-                {
-                    notification.State = ProgressNotificationState.Cancelled;
-                    return imported;
-                }
-            }
-
-            if (imported.Count == 0)
-            {
-                notification.Text = $"{HumanisedModelName.Humanize(LetterCasing.Title)} import failed!";
-                notification.State = ProgressNotificationState.Cancelled;
-            }
-            else
-            {
-                notification.CompletionText = imported.Count == 1
-                    ? $"Imported {imported.First().Value.GetDisplayString()}!"
-                    : $"Imported {imported.Count} {HumanisedModelName}s!";
-
-                if (imported.Count > 0 && PostImport != null)
-                {
-                    notification.CompletionText += " Click to view.";
-                    notification.CompletionClickAction = () =>
-                    {
-                        PostImport?.Invoke(imported);
-                        return true;
-                    };
-                }
-
-                notification.State = ProgressNotificationState.Completed;
-            }
-
-            return imported;
-        }
-
-        /// <summary>
-        /// Import one <typeparamref name="TModel"/> from the filesystem and delete the file on success.
-        /// Note that this bypasses the UI flow and should only be used for special cases or testing.
-        /// </summary>
-        /// <param name="task">The <see cref="ImportTask"/> containing data about the <typeparamref name="TModel"/> to import.</param>
-        /// <param name="lowPriority">Whether this is a low priority import.</param>
-        /// <param name="cancellationToken">An optional cancellation token.</param>
-        /// <returns>The imported model, if successful.</returns>
-        public async Task<ILive<TModel>> Import(ImportTask task, bool lowPriority = false, CancellationToken cancellationToken = default)
-        {
-            cancellationToken.ThrowIfCancellationRequested();
-
-            ILive<TModel> import;
-            using (ArchiveReader reader = task.GetReader())
-                import = await Import(reader, lowPriority, cancellationToken).ConfigureAwait(false);
-
-            // We may or may not want to delete the file depending on where it is stored.
-            //  e.g. reconstructing/repairing database with items from default storage.
-            // Also, not always a single file, i.e. for LegacyFilesystemReader
-            // TODO: Add a check to prevent files from storage to be deleted.
-            try
-            {
-                if (import != null && File.Exists(task.Path) && ShouldDeleteArchive(task.Path))
-                    File.Delete(task.Path);
-            }
-            catch (Exception e)
-            {
-                LogForModel(import?.Value, $@"Could not delete original file after import ({task})", e);
-            }
-
-            return import;
-        }
-
-        public Action<IEnumerable<ILive<TModel>>> PostImport { protected get; set; }
-
-        /// <summary>
-        /// Silently import an item from an <see cref="ArchiveReader"/>.
-        /// </summary>
-        /// <param name="archive">The archive to be imported.</param>
-        /// <param name="lowPriority">Whether this is a low priority import.</param>
-        /// <param name="cancellationToken">An optional cancellation token.</param>
-        public Task<ILive<TModel>> Import(ArchiveReader archive, bool lowPriority = false, CancellationToken cancellationToken = default)
-        {
-            cancellationToken.ThrowIfCancellationRequested();
-
-            TModel model = null;
-
-            try
-            {
-                model = CreateModel(archive);
-
-                if (model == null)
-                    return Task.FromResult<ILive<TModel>>(null);
-            }
-            catch (TaskCanceledException)
-            {
-                throw;
-            }
-            catch (Exception e)
-            {
-                LogForModel(model, @$"Model creation of {archive.Name} failed.", e);
-                return null;
-            }
-
-            return Import(model, archive, lowPriority, cancellationToken);
-        }
-
-        /// <summary>
-        /// Any file extensions which should be included in hash creation.
-        /// Generally should include all file types which determine the file's uniqueness.
-        /// Large files should be avoided if possible.
-        /// </summary>
-        /// <remarks>
-        /// This is only used by the default hash implementation. If <see cref="ComputeHash"/> is overridden, it will not be used.
-        /// </remarks>
-        protected abstract string[] HashableFileTypes { get; }
-
-        internal static void LogForModel(TModel model, string message, Exception e = null)
-        {
-            string prefix = $"[{(model?.Hash ?? "?????").Substring(0, 5)}]";
-
-            if (e != null)
-                Logger.Error(e, $"{prefix} {message}", LoggingTarget.Database);
-            else
-                Logger.Log($"{prefix} {message}", LoggingTarget.Database);
-        }
-
-        /// <summary>
-        /// Whether the implementation overrides <see cref="ComputeHash"/> with a custom implementation.
-        /// Custom hash implementations must bypass the early exit in the import flow (see <see cref="computeHashFast"/> usage).
-        /// </summary>
-        protected virtual bool HasCustomHashFunction => false;
-
-        /// <summary>
-        /// Create a SHA-2 hash from the provided archive based on file content of all files matching <see cref="HashableFileTypes"/>.
-        /// </summary>
-        /// <remarks>
-        ///  In the case of no matching files, a hash will be generated from the passed archive's <see cref="ArchiveReader.Name"/>.
-        /// </remarks>
-        protected virtual string ComputeHash(TModel item)
-        {
-            var hashableFiles = item.Files
-                                    .Where(f => HashableFileTypes.Any(ext => f.Filename.EndsWith(ext, StringComparison.OrdinalIgnoreCase)))
-                                    .OrderBy(f => f.Filename)
-                                    .ToArray();
-
-            if (hashableFiles.Length > 0)
-            {
-                // for now, concatenate all hashable files in the set to create a unique hash.
-                MemoryStream hashable = new MemoryStream();
-
-                foreach (TFileModel file in hashableFiles)
-                {
-                    using (Stream s = Files.Store.GetStream(file.FileInfo.GetStoragePath()))
-                        s.CopyTo(hashable);
-                }
-
-                if (hashable.Length > 0)
-                    return hashable.ComputeSHA2Hash();
-            }
-
-            return generateFallbackHash();
-        }
-
-        /// <summary>
-        /// Silently import an item from a <typeparamref name="TModel"/>.
-        /// </summary>
-        /// <param name="item">The model to be imported.</param>
-        /// <param name="archive">An optional archive to use for model population.</param>
-        /// <param name="lowPriority">Whether this is a low priority import.</param>
-        /// <param name="cancellationToken">An optional cancellation token.</param>
-        public virtual async Task<ILive<TModel>> Import(TModel item, ArchiveReader archive = null, bool lowPriority = false, CancellationToken cancellationToken = default) => await Task.Factory.StartNew(async () =>
-        {
-            cancellationToken.ThrowIfCancellationRequested();
-
-            bool checkedExisting = false;
-            TModel existing = null;
-
-            if (archive != null && !HasCustomHashFunction)
-            {
-                // this is a fast bail condition to improve large import performance.
-                item.Hash = computeHashFast(archive);
-
-                checkedExisting = true;
-                existing = CheckForExisting(item);
-
-                if (existing != null)
-                {
-                    // bare minimum comparisons
-                    //
-                    // note that this should really be checking filesizes on disk (of existing files) for some degree of sanity.
-                    // or alternatively doing a faster hash check. either of these require database changes and reprocessing of existing files.
-                    if (CanSkipImport(existing, item) &&
-                        getFilenames(existing.Files).SequenceEqual(getShortenedFilenames(archive).Select(p => p.shortened).OrderBy(f => f)))
-                    {
-                        LogForModel(item, @$"Found existing (optimised) {HumanisedModelName} for {item} (ID {existing.ID}) – skipping import.");
-                        Undelete(existing);
-                        return existing.ToEntityFrameworkLive();
-                    }
-
-                    LogForModel(item, @"Found existing (optimised) but failed pre-check.");
-                }
-            }
-
-            void rollback()
-            {
-                if (!Delete(item))
-                {
-                    // We may have not yet added the model to the underlying table, but should still clean up files.
-                    LogForModel(item, @"Dereferencing files for incomplete import.");
-                    Files.Dereference(item.Files.Select(f => f.FileInfo).ToArray());
-                }
-            }
-
-            delayEvents();
-
-            try
-            {
-                LogForModel(item, @"Beginning import...");
-
-                if (archive != null)
-                    item.Files.AddRange(createFileInfos(archive, Files));
-                item.Hash = ComputeHash(item);
-
-                await Populate(item, archive, cancellationToken).ConfigureAwait(false);
-
-                using (var write = ContextFactory.GetForWrite()) // used to share a context for full import. keep in mind this will block all writes.
-                {
-                    try
-                    {
-                        if (!write.IsTransactionLeader) throw new InvalidOperationException(@$"Ensure there is no parent transaction so errors can correctly be handled by {this}");
-
-                        if (!checkedExisting)
-                            existing = CheckForExisting(item);
-
-                        if (existing != null)
-                        {
-                            if (CanReuseExisting(existing, item))
-                            {
-                                Undelete(existing);
-                                LogForModel(item, @$"Found existing {HumanisedModelName} for {item} (ID {existing.ID}) – skipping import.");
-                                // existing item will be used; rollback new import and exit early.
-                                rollback();
-                                flushEvents(true);
-                                return existing.ToEntityFrameworkLive();
-                            }
-
-                            LogForModel(item, @"Found existing but failed re-use check.");
-                            Delete(existing);
-                            ModelStore.PurgeDeletable(s => s.ID == existing.ID);
-                        }
-
-                        PreImport(item);
-
-                        // import to store
-                        ModelStore.Add(item);
-                    }
-                    catch (Exception e)
-                    {
-                        write.Errors.Add(e);
-                        throw;
-                    }
-                }
-
-                LogForModel(item, @"Import successfully completed!");
-            }
-            catch (Exception e)
-            {
-                if (!(e is TaskCanceledException))
-                    LogForModel(item, @"Database import or population failed and has been rolled back.", e);
-
-                rollback();
-                flushEvents(false);
-                throw;
-            }
-
-            flushEvents(true);
-            return item.ToEntityFrameworkLive();
-        }, cancellationToken, TaskCreationOptions.HideScheduler, lowPriority ? import_scheduler_low_priority : import_scheduler).Unwrap().ConfigureAwait(false);
-
-        /// <summary>
-        /// Replace an existing file with a new version.
-        /// </summary>
-        /// <param name="model">The item to operate on.</param>
-        /// <param name="file">The existing file to be replaced.</param>
-        /// <param name="contents">The new file contents.</param>
-        public void ReplaceFile(TModel model, TFileModel file, Stream contents)
-        {
-            using (ContextFactory.GetForWrite())
-            {
-                DeleteFile(model, file);
-                AddFile(model, contents, file.Filename);
-            }
-        }
-
-        /// <summary>
-        /// Delete an existing file.
-        /// </summary>
-        /// <param name="model">The item to operate on.</param>
-        /// <param name="file">The existing file to be deleted.</param>
-        public void DeleteFile(TModel model, TFileModel file)
-        {
-            using (var usage = ContextFactory.GetForWrite())
-            {
-                // Dereference the existing file info, since the file model will be removed.
-                if (file.FileInfo != null)
-                {
-                    Files.Dereference(file.FileInfo);
-
-                    if (file.IsManaged)
-                    {
-                        // This shouldn't be required, but here for safety in case the provided TModel is not being change tracked
-                        // Definitely can be removed once we rework the database backend.
-                        usage.Context.Set<TFileModel>().Remove(file);
-                    }
-                }
-
-                model.Files.Remove(file);
-            }
-        }
-
-        /// <summary>
-        /// Add a new file.
-        /// </summary>
-        /// <param name="model">The item to operate on.</param>
-        /// <param name="contents">The new file contents.</param>
-        /// <param name="filename">The filename for the new file.</param>
-        public void AddFile(TModel model, Stream contents, string filename)
-        {
-            using (ContextFactory.GetForWrite())
-            {
-                model.Files.Add(new TFileModel
-                {
-                    Filename = filename,
-                    FileInfo = Files.Add(contents)
-                });
-            }
-
-            if (model.IsManaged)
-                Update(model);
-        }
-
-        /// <summary>
-        /// Perform an update of the specified item.
-        /// TODO: Support file additions/removals.
-        /// </summary>
-        /// <param name="item">The item to update.</param>
-        public void Update(TModel item)
-        {
-            using (ContextFactory.GetForWrite())
-            {
-                item.Hash = ComputeHash(item);
-                ModelStore.Update(item);
-            }
-        }
-
-        /// <summary>
-        /// Delete an item from the manager.
-        /// Is a no-op for already deleted items.
-        /// </summary>
-        /// <param name="item">The item to delete.</param>
-        /// <returns>false if no operation was performed</returns>
-        public bool Delete(TModel item)
-        {
-            using (ContextFactory.GetForWrite())
-            {
-                // re-fetch the model on the import context.
-                var foundModel = queryModel().Include(s => s.Files).ThenInclude(f => f.FileInfo).FirstOrDefault(s => s.ID == item.ID);
-
-                if (foundModel == null || foundModel.DeletePending) return false;
-
-                if (ModelStore.Delete(foundModel))
-                    Files.Dereference(foundModel.Files.Select(f => f.FileInfo).ToArray());
-                return true;
-            }
-        }
-
-        /// <summary>
-        /// Delete multiple items.
-        /// This will post notifications tracking progress.
-        /// </summary>
-        public void Delete(List<TModel> items, bool silent = false)
-        {
-            if (items.Count == 0) return;
-
-            var notification = new ProgressNotification
-            {
-                Progress = 0,
-                Text = $"Preparing to delete all {HumanisedModelName}s...",
-                CompletionText = $"Deleted all {HumanisedModelName}s!",
-                State = ProgressNotificationState.Active,
-            };
-
-            if (!silent)
-                PostNotification?.Invoke(notification);
-
-            int i = 0;
-
-            foreach (var b in items)
-            {
-                if (notification.State == ProgressNotificationState.Cancelled)
-                    // user requested abort
-                    return;
-
-                notification.Text = $"Deleting {HumanisedModelName}s ({++i} of {items.Count})";
-
-                Delete(b);
-
-                notification.Progress = (float)i / items.Count;
-            }
-
-            notification.State = ProgressNotificationState.Completed;
-        }
-
-        /// <summary>
-        /// Restore multiple items that were previously deleted.
-        /// This will post notifications tracking progress.
-        /// </summary>
-        public void Undelete(List<TModel> items, bool silent = false)
-        {
-            if (!items.Any()) return;
-
-            var notification = new ProgressNotification
-            {
-                CompletionText = "Restored all deleted items!",
-                Progress = 0,
-                State = ProgressNotificationState.Active,
-            };
-
-            if (!silent)
-                PostNotification?.Invoke(notification);
-
-            int i = 0;
-
-            foreach (var item in items)
-            {
-                if (notification.State == ProgressNotificationState.Cancelled)
-                    // user requested abort
-                    return;
-
-                notification.Text = $"Restoring ({++i} of {items.Count})";
-
-                Undelete(item);
-
-                notification.Progress = (float)i / items.Count;
-            }
-
-            notification.State = ProgressNotificationState.Completed;
-        }
-
-        /// <summary>
-        /// Restore an item that was previously deleted. Is a no-op if the item is not in a deleted state, or has its protected flag set.
-        /// </summary>
-        /// <param name="item">The item to restore</param>
-        public void Undelete(TModel item)
-        {
-            using (var usage = ContextFactory.GetForWrite())
-            {
-                usage.Context.ChangeTracker.AutoDetectChangesEnabled = false;
-
-                if (!ModelStore.Undelete(item)) return;
-
-                Files.Reference(item.Files.Select(f => f.FileInfo).ToArray());
-
-                usage.Context.ChangeTracker.AutoDetectChangesEnabled = true;
-            }
-        }
-
-        private string computeHashFast(ArchiveReader reader)
-        {
-            MemoryStream hashable = new MemoryStream();
-
-            foreach (string file in reader.Filenames.Where(f => HashableFileTypes.Any(ext => f.EndsWith(ext, StringComparison.OrdinalIgnoreCase))).OrderBy(f => f))
-            {
-                using (Stream s = reader.GetStream(file))
-                    s.CopyTo(hashable);
-            }
-
-            if (hashable.Length > 0)
-                return hashable.ComputeSHA2Hash();
-
-            return generateFallbackHash();
-        }
-
-        /// <summary>
-        /// Create all required <see cref="IO.FileInfo"/>s for the provided archive, adding them to the global file store.
-        /// </summary>
-        private List<TFileModel> createFileInfos(ArchiveReader reader, FileStore files)
-        {
-            var fileInfos = new List<TFileModel>();
-
-            // import files to manager
-            foreach (var filenames in getShortenedFilenames(reader))
-            {
-                using (Stream s = reader.GetStream(filenames.original))
-                {
-                    fileInfos.Add(new TFileModel
-                    {
-                        Filename = filenames.shortened,
-                        FileInfo = files.Add(s)
-                    });
-                }
-            }
-
-            return fileInfos;
-        }
-
-        private IEnumerable<(string original, string shortened)> getShortenedFilenames(ArchiveReader reader)
-        {
-            string prefix = reader.Filenames.GetCommonPrefix();
-            if (!(prefix.EndsWith('/') || prefix.EndsWith('\\')))
-                prefix = string.Empty;
-
-            // import files to manager
-            foreach (string file in reader.Filenames)
-                yield return (file, file.Substring(prefix.Length).ToStandardisedPath());
-        }
-
-        #region osu-stable import
-
-        /// <summary>
-        /// Whether this specified path should be removed after successful import.
-        /// </summary>
-        /// <param name="path">The path for consideration. May be a file or a directory.</param>
-        /// <returns>Whether to perform deletion.</returns>
-        protected virtual bool ShouldDeleteArchive(string path) => false;
-
-        #endregion
-
-        /// <summary>
-        /// Create a barebones model from the provided archive.
-        /// Actual expensive population should be done in <see cref="Populate"/>; this should just prepare for duplicate checking.
-        /// </summary>
-        /// <param name="archive">The archive to create the model for.</param>
-        /// <returns>A model populated with minimal information. Returning a null will abort importing silently.</returns>
-        protected abstract TModel CreateModel(ArchiveReader archive);
-
-        /// <summary>
-        /// Populate the provided model completely from the given archive.
-        /// After this method, the model should be in a state ready to commit to a store.
-        /// </summary>
-        /// <param name="model">The model to populate.</param>
-        /// <param name="archive">The archive to use as a reference for population. May be null.</param>
-        /// <param name="cancellationToken">An optional cancellation token.</param>
-        protected abstract Task Populate(TModel model, [CanBeNull] ArchiveReader archive, CancellationToken cancellationToken = default);
-
-        /// <summary>
-        /// Perform any final actions before the import to database executes.
-        /// </summary>
-        /// <param name="model">The model prepared for import.</param>
-        protected virtual void PreImport(TModel model)
-        {
-        }
-
-        /// <summary>
-        /// Check whether an existing model already exists for a new import item.
-        /// </summary>
-        /// <param name="model">The new model proposed for import.</param>
-        /// <returns>An existing model which matches the criteria to skip importing, else null.</returns>
-        protected TModel CheckForExisting(TModel model) => model.Hash == null ? null : ModelStore.ConsumableItems.FirstOrDefault(b => b.Hash == model.Hash);
-
-        public bool IsAvailableLocally(TModel model) => CheckLocalAvailability(model, ModelStore.ConsumableItems.Where(m => !m.DeletePending));
-
-        /// <summary>
-        /// Performs implementation specific comparisons to determine whether a given model is present in the local store.
-        /// </summary>
-        /// <param name="model">The <typeparamref name="TModel"/> whose existence needs to be checked.</param>
-        /// <param name="items">The usable items present in the store.</param>
-        /// <returns>Whether the <typeparamref name="TModel"/> exists.</returns>
-        protected virtual bool CheckLocalAvailability(TModel model, IQueryable<TModel> items)
-            => model.IsManaged && items.Any(i => i.ID == model.ID && i.Files.Any());
-
-        /// <summary>
-        /// Whether import can be skipped after finding an existing import early in the process.
-        /// Only valid when <see cref="ComputeHash"/> is not overridden.
-        /// </summary>
-        /// <param name="existing">The existing model.</param>
-        /// <param name="import">The newly imported model.</param>
-        /// <returns>Whether to skip this import completely.</returns>
-        protected virtual bool CanSkipImport(TModel existing, TModel import) => true;
-
-        /// <summary>
-        /// After an existing <typeparamref name="TModel"/> is found during an import process, the default behaviour is to use/restore the existing
-        /// item and skip the import. This method allows changing that behaviour.
-        /// </summary>
-        /// <param name="existing">The existing model.</param>
-        /// <param name="import">The newly imported model.</param>
-        /// <returns>Whether the existing model should be restored and used. Returning false will delete the existing and force a re-import.</returns>
-        protected virtual bool CanReuseExisting(TModel existing, TModel import) =>
-            // for the best or worst, we copy and import files of a new import before checking whether
-            // it is a duplicate. so to check if anything has changed, we can just compare all FileInfo IDs.
-            getIDs(existing.Files).SequenceEqual(getIDs(import.Files)) &&
-            getFilenames(existing.Files).SequenceEqual(getFilenames(import.Files));
-
-        private IEnumerable<long> getIDs(List<TFileModel> files)
-        {
-            foreach (var f in files.OrderBy(f => f.Filename))
-                yield return f.FileInfo.ID;
-        }
-
-        private IEnumerable<string> getFilenames(List<TFileModel> files)
-        {
-            foreach (var f in files.OrderBy(f => f.Filename))
-                yield return f.Filename;
-        }
-
-        private DbSet<TModel> queryModel() => ContextFactory.Get().Set<TModel>();
-
-        public virtual string HumanisedModelName => $"{typeof(TModel).Name.Replace(@"Info", "").ToLower()}";
-
-        #region Event handling / delaying
-
-        private readonly List<Action> queuedEvents = new List<Action>();
-
-        /// <summary>
-        /// Allows delaying of outwards events until an operation is confirmed (at a database level).
-        /// </summary>
-        private bool delayingEvents;
-
-        /// <summary>
-        /// Begin delaying outwards events.
-        /// </summary>
-        private void delayEvents() => delayingEvents = true;
-
-        /// <summary>
-        /// Flush delayed events and disable delaying.
-        /// </summary>
-        /// <param name="perform">Whether the flushed events should be performed.</param>
-        private void flushEvents(bool perform)
-        {
-            Action[] events;
-
-            lock (queuedEvents)
-            {
-                events = queuedEvents.ToArray();
-                queuedEvents.Clear();
-            }
-
-            if (perform)
-            {
-                foreach (var a in events)
-                    a.Invoke();
-            }
-
-            delayingEvents = false;
-        }
-
-        private void handleEvent(Action a)
-        {
-            if (delayingEvents)
-            {
-                lock (queuedEvents)
-                    queuedEvents.Add(a);
-            }
-            else
-                a.Invoke();
-        }
-
-        #endregion
-
-        private static string generateFallbackHash()
-        {
-            // if a hash could no be generated from file content, presume a unique / new import.
-            // therefore, let's use a guaranteed unique hash.
-            // this doesn't follow the SHA2 hashing schema intentionally, so such entries on the data store can be identified.
-            return Guid.NewGuid().ToString();
-        }
-    }
-}
diff --git a/osu.Game/Database/DatabaseBackedStore.cs b/osu.Game/Database/DatabaseBackedStore.cs
deleted file mode 100644
index 03e1c014b2..0000000000
--- a/osu.Game/Database/DatabaseBackedStore.cs
+++ /dev/null
@@ -1,52 +0,0 @@
-// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using System.Linq;
-using Microsoft.EntityFrameworkCore;
-using osu.Framework.Platform;
-
-namespace osu.Game.Database
-{
-    public abstract class DatabaseBackedStore
-    {
-        protected readonly Storage Storage;
-
-        protected readonly IDatabaseContextFactory ContextFactory;
-
-        /// <summary>
-        /// Refresh an instance potentially from a different thread with a local context-tracked instance.
-        /// </summary>
-        /// <param name="obj">The object to use as a reference when negotiating a local instance.</param>
-        /// <param name="lookupSource">An optional lookup source which will be used to query and populate a freshly retrieved replacement. If not provided, the refreshed object will still be returned but will not have any includes.</param>
-        /// <typeparam name="T">A valid EF-stored type.</typeparam>
-        protected void Refresh<T>(ref T obj, IQueryable<T> lookupSource = null) where T : class, IHasPrimaryKey
-        {
-            using (var usage = ContextFactory.GetForWrite())
-            {
-                var context = usage.Context;
-
-                if (context.Entry(obj).State != EntityState.Detached) return;
-
-                int id = obj.ID;
-                var foundObject = lookupSource?.SingleOrDefault(t => t.ID == id) ?? context.Find<T>(id);
-                if (foundObject != null)
-                    obj = foundObject;
-                else
-                    context.Add(obj);
-            }
-        }
-
-        protected DatabaseBackedStore(IDatabaseContextFactory contextFactory, Storage storage = null)
-        {
-            ContextFactory = contextFactory;
-            Storage = storage;
-        }
-
-        /// <summary>
-        /// Perform any common clean-up tasks. Should be run when idle, or whenever necessary.
-        /// </summary>
-        public virtual void Cleanup()
-        {
-        }
-    }
-}
diff --git a/osu.Game/Database/DatabaseContextFactory.cs b/osu.Game/Database/DatabaseContextFactory.cs
index 94fa967d72..635c4373cd 100644
--- a/osu.Game/Database/DatabaseContextFactory.cs
+++ b/osu.Game/Database/DatabaseContextFactory.cs
@@ -1,9 +1,12 @@
 // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
 // See the LICENCE file in the repository root for full licence text.
 
+using System.IO;
 using System.Linq;
 using System.Threading;
 using Microsoft.EntityFrameworkCore.Storage;
+using osu.Framework.Development;
+using osu.Framework.Logging;
 using osu.Framework.Platform;
 using osu.Framework.Statistics;
 
@@ -13,7 +16,7 @@ namespace osu.Game.Database
     {
         private readonly Storage storage;
 
-        private const string database_name = @"client.db";
+        public const string DATABASE_NAME = @"client.db";
 
         private ThreadLocal<OsuDbContext> threadContexts;
 
@@ -139,11 +142,23 @@ namespace osu.Game.Database
             threadContexts = new ThreadLocal<OsuDbContext>(CreateContext, true);
         }
 
-        protected virtual OsuDbContext CreateContext() => new OsuDbContext(CreateDatabaseConnectionString(database_name, storage))
+        protected virtual OsuDbContext CreateContext() => new OsuDbContext(CreateDatabaseConnectionString(DATABASE_NAME, storage))
         {
             Database = { AutoTransactionsEnabled = false }
         };
 
+        public void CreateBackup(string backupFilename)
+        {
+            Logger.Log($"Creating full EF database backup at {backupFilename}", LoggingTarget.Database);
+
+            if (DebugUtils.IsDebugBuild)
+                Logger.Log("Your development database has been fully migrated to realm. If you switch back to a pre-realm branch and need your previous database, rename the backup file back to \"client.db\".\n\nNote that doing this can potentially leave your file store in a bad state.", level: LogLevel.Important);
+
+            using (var source = storage.GetStream(DATABASE_NAME))
+            using (var destination = storage.GetStream(backupFilename, FileAccess.Write, FileMode.CreateNew))
+                source.CopyTo(destination);
+        }
+
         public void ResetDatabase()
         {
             lock (writeLock)
@@ -152,7 +167,7 @@ namespace osu.Game.Database
 
                 try
                 {
-                    storage.Delete(database_name);
+                    storage.Delete(DATABASE_NAME);
                 }
                 catch
                 {
diff --git a/osu.Game/Database/EFToRealmMigrator.cs b/osu.Game/Database/EFToRealmMigrator.cs
index b79a982460..1de6c25070 100644
--- a/osu.Game/Database/EFToRealmMigrator.cs
+++ b/osu.Game/Database/EFToRealmMigrator.cs
@@ -1,11 +1,17 @@
 // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
 // See the LICENCE file in the repository root for full licence text.
 
+using System;
 using System.Linq;
 using Microsoft.EntityFrameworkCore;
+using osu.Framework.Logging;
+using osu.Game.Beatmaps;
 using osu.Game.Configuration;
 using osu.Game.Models;
+using osu.Game.Rulesets;
+using osu.Game.Scoring;
 using osu.Game.Skinning;
+using Realms;
 
 #nullable enable
 
@@ -17,6 +23,8 @@ namespace osu.Game.Database
         private readonly RealmContextFactory realmContextFactory;
         private readonly OsuConfigManager config;
 
+        private bool hasTakenBackup;
+
         public EFToRealmMigrator(DatabaseContextFactory efContextFactory, RealmContextFactory realmContextFactory, OsuConfigManager config)
         {
             this.efContextFactory = efContextFactory;
@@ -26,10 +34,229 @@ namespace osu.Game.Database
 
         public void Run()
         {
-            using (var db = efContextFactory.GetForWrite())
+            using (var ef = efContextFactory.GetForWrite())
             {
-                migrateSettings(db);
-                migrateSkins(db);
+                migrateSettings(ef);
+                migrateSkins(ef);
+                migrateBeatmaps(ef);
+                migrateScores(ef);
+            }
+
+            // Delete the database permanently.
+            // Will cause future startups to not attempt migration.
+            Logger.Log("Migration successful, deleting EF database", LoggingTarget.Database);
+            efContextFactory.ResetDatabase();
+        }
+
+        private void migrateBeatmaps(DatabaseWriteUsage ef)
+        {
+            // can be removed 20220730.
+            var existingBeatmapSets = ef.Context.EFBeatmapSetInfo
+                                        .Include(s => s.Beatmaps).ThenInclude(b => b.RulesetInfo)
+                                        .Include(s => s.Beatmaps).ThenInclude(b => b.Metadata)
+                                        .Include(s => s.Beatmaps).ThenInclude(b => b.BaseDifficulty)
+                                        .Include(s => s.Files).ThenInclude(f => f.FileInfo)
+                                        .Include(s => s.Metadata);
+
+            Logger.Log("Beginning beatmaps migration to realm", LoggingTarget.Database);
+
+            // previous entries in EF are removed post migration.
+            if (!existingBeatmapSets.Any())
+            {
+                Logger.Log("No beatmaps found to migrate", LoggingTarget.Database);
+                return;
+            }
+
+            int count = existingBeatmapSets.Count();
+
+            using (var realm = realmContextFactory.CreateContext())
+            {
+                Logger.Log($"Found {count} beatmaps in EF", LoggingTarget.Database);
+
+                if (!hasTakenBackup)
+                {
+                    string migration = $"before_beatmap_migration_{DateTimeOffset.UtcNow.ToUnixTimeSeconds()}";
+
+                    efContextFactory.CreateBackup($"client.{migration}.db");
+                    realmContextFactory.CreateBackup($"client.{migration}.realm");
+
+                    hasTakenBackup = true;
+                }
+
+                // only migrate data if the realm database is empty.
+                // note that this cannot be written as: `realm.All<BeatmapSetInfo>().All(s => s.Protected)`, because realm does not support `.All()`.
+                if (realm.All<BeatmapSetInfo>().Any(s => !s.Protected))
+                {
+                    Logger.Log("Skipping migration as realm already has beatmaps loaded", LoggingTarget.Database);
+                }
+                else
+                {
+                    using (var transaction = realm.BeginWrite())
+                    {
+                        foreach (var beatmapSet in existingBeatmapSets)
+                        {
+                            var realmBeatmapSet = new BeatmapSetInfo
+                            {
+                                OnlineID = beatmapSet.OnlineID ?? -1,
+                                DateAdded = beatmapSet.DateAdded,
+                                Status = beatmapSet.Status,
+                                DeletePending = beatmapSet.DeletePending,
+                                Hash = beatmapSet.Hash,
+                                Protected = beatmapSet.Protected,
+                            };
+
+                            migrateFiles(beatmapSet, realm, realmBeatmapSet);
+
+                            foreach (var beatmap in beatmapSet.Beatmaps)
+                            {
+                                var realmBeatmap = new BeatmapInfo
+                                {
+                                    DifficultyName = beatmap.DifficultyName,
+                                    Status = beatmap.Status,
+                                    OnlineID = beatmap.OnlineID ?? -1,
+                                    Length = beatmap.Length,
+                                    BPM = beatmap.BPM,
+                                    Hash = beatmap.Hash,
+                                    StarRating = beatmap.StarRating,
+                                    MD5Hash = beatmap.MD5Hash,
+                                    Hidden = beatmap.Hidden,
+                                    AudioLeadIn = beatmap.AudioLeadIn,
+                                    StackLeniency = beatmap.StackLeniency,
+                                    SpecialStyle = beatmap.SpecialStyle,
+                                    LetterboxInBreaks = beatmap.LetterboxInBreaks,
+                                    WidescreenStoryboard = beatmap.WidescreenStoryboard,
+                                    EpilepsyWarning = beatmap.EpilepsyWarning,
+                                    SamplesMatchPlaybackRate = beatmap.SamplesMatchPlaybackRate,
+                                    DistanceSpacing = beatmap.DistanceSpacing,
+                                    BeatDivisor = beatmap.BeatDivisor,
+                                    GridSize = beatmap.GridSize,
+                                    TimelineZoom = beatmap.TimelineZoom,
+                                    Countdown = beatmap.Countdown,
+                                    CountdownOffset = beatmap.CountdownOffset,
+                                    MaxCombo = beatmap.MaxCombo,
+                                    Bookmarks = beatmap.Bookmarks,
+                                    Ruleset = realm.Find<RulesetInfo>(beatmap.RulesetInfo.ShortName),
+                                    Difficulty = new BeatmapDifficulty(beatmap.BaseDifficulty),
+                                    Metadata = getBestMetadata(beatmap.Metadata, beatmapSet.Metadata),
+                                    BeatmapSet = realmBeatmapSet,
+                                };
+
+                                realmBeatmapSet.Beatmaps.Add(realmBeatmap);
+                            }
+
+                            realm.Add(realmBeatmapSet);
+                        }
+
+                        transaction.Commit();
+                        Logger.Log($"Successfully migrated {count} beatmaps to realm", LoggingTarget.Database);
+                    }
+                }
+            }
+        }
+
+        private BeatmapMetadata getBestMetadata(EFBeatmapMetadata? beatmapMetadata, EFBeatmapMetadata? beatmapSetMetadata)
+        {
+            var metadata = beatmapMetadata ?? beatmapSetMetadata ?? new EFBeatmapMetadata();
+
+            return new BeatmapMetadata
+            {
+                Title = metadata.Title,
+                TitleUnicode = metadata.TitleUnicode,
+                Artist = metadata.Artist,
+                ArtistUnicode = metadata.ArtistUnicode,
+                Author = new RealmUser
+                {
+                    OnlineID = metadata.Author.Id,
+                    Username = metadata.Author.Username,
+                },
+                Source = metadata.Source,
+                Tags = metadata.Tags,
+                PreviewTime = metadata.PreviewTime,
+                AudioFile = metadata.AudioFile,
+                BackgroundFile = metadata.BackgroundFile,
+            };
+        }
+
+        private void migrateScores(DatabaseWriteUsage db)
+        {
+            // can be removed 20220730.
+            var existingScores = db.Context.ScoreInfo
+                                   .Include(s => s.Ruleset)
+                                   .Include(s => s.BeatmapInfo)
+                                   .Include(s => s.Files)
+                                   .ThenInclude(f => f.FileInfo);
+
+            Logger.Log("Beginning scores migration to realm", LoggingTarget.Database);
+
+            // previous entries in EF are removed post migration.
+            if (!existingScores.Any())
+            {
+                Logger.Log("No scores found to migrate", LoggingTarget.Database);
+                return;
+            }
+
+            int count = existingScores.Count();
+
+            using (var realm = realmContextFactory.CreateContext())
+            {
+                Logger.Log($"Found {count} scores in EF", LoggingTarget.Database);
+
+                if (!hasTakenBackup)
+                {
+                    string migration = $"before_score_migration_{DateTimeOffset.UtcNow.ToUnixTimeSeconds()}";
+
+                    efContextFactory.CreateBackup($"client.{migration}.db");
+                    realmContextFactory.CreateBackup($"client.{migration}.realm");
+
+                    hasTakenBackup = true;
+                }
+
+                // only migrate data if the realm database is empty.
+                if (realm.All<ScoreInfo>().Any())
+                {
+                    Logger.Log("Skipping migration as realm already has scores loaded", LoggingTarget.Database);
+                }
+                else
+                {
+                    using (var transaction = realm.BeginWrite())
+                    {
+                        foreach (var score in existingScores)
+                        {
+                            var realmScore = new ScoreInfo
+                            {
+                                Hash = score.Hash,
+                                DeletePending = score.DeletePending,
+                                OnlineID = score.OnlineID ?? -1,
+                                ModsJson = score.ModsJson,
+                                StatisticsJson = score.StatisticsJson,
+                                User = score.User,
+                                TotalScore = score.TotalScore,
+                                MaxCombo = score.MaxCombo,
+                                Accuracy = score.Accuracy,
+                                HasReplay = ((IScoreInfo)score).HasReplay,
+                                Date = score.Date,
+                                PP = score.PP,
+                                BeatmapInfo = realm.All<BeatmapInfo>().First(b => b.Hash == score.BeatmapInfo.Hash),
+                                Ruleset = realm.Find<RulesetInfo>(score.Ruleset.ShortName),
+                                Rank = score.Rank,
+                                HitEvents = score.HitEvents,
+                                Passed = score.Passed,
+                                Combo = score.Combo,
+                                Position = score.Position,
+                                Statistics = score.Statistics,
+                                Mods = score.Mods,
+                                APIMods = score.APIMods,
+                            };
+
+                            migrateFiles(score, realm, realmScore);
+
+                            realm.Add(realmScore);
+                        }
+
+                        transaction.Commit();
+                        Logger.Log($"Successfully migrated {count} scores to realm", LoggingTarget.Database);
+                    }
+                }
             }
         }
 
@@ -66,6 +293,8 @@ namespace osu.Game.Database
                 // note that this cannot be written as: `realm.All<SkinInfo>().All(s => s.Protected)`, because realm does not support `.All()`.
                 if (!realm.All<SkinInfo>().Any(s => !s.Protected))
                 {
+                    Logger.Log($"Migrating {existingSkins.Count} skins", LoggingTarget.Database);
+
                     foreach (var skin in existingSkins)
                     {
                         var realmSkin = new SkinInfo
@@ -77,15 +306,7 @@ namespace osu.Game.Database
                             InstantiationInfo = skin.InstantiationInfo,
                         };
 
-                        foreach (var file in skin.Files)
-                        {
-                            var realmFile = realm.Find<RealmFile>(file.FileInfo.Hash);
-
-                            if (realmFile == null)
-                                realm.Add(realmFile = new RealmFile { Hash = file.FileInfo.Hash });
-
-                            realmSkin.Files.Add(new RealmNamedFileUsage(realmFile, file.Filename));
-                        }
+                        migrateFiles(skin, realm, realmSkin);
 
                         realm.Add(realmSkin);
 
@@ -94,28 +315,42 @@ namespace osu.Game.Database
                     }
                 }
 
-                db.Context.RemoveRange(existingSkins);
-                // Intentionally don't clean up the files, so they don't get purged by EF.
-
                 transaction.Commit();
             }
         }
 
+        private static void migrateFiles<T>(IHasFiles<T> fileSource, Realm realm, IHasRealmFiles realmObject) where T : INamedFileInfo
+        {
+            foreach (var file in fileSource.Files)
+            {
+                var realmFile = realm.Find<RealmFile>(file.FileInfo.Hash);
+
+                if (realmFile == null)
+                    realm.Add(realmFile = new RealmFile { Hash = file.FileInfo.Hash });
+
+                realmObject.Files.Add(new RealmNamedFileUsage(realmFile, file.Filename));
+            }
+        }
+
         private void migrateSettings(DatabaseWriteUsage db)
         {
             // migrate ruleset settings. can be removed 20220315.
-            var existingSettings = db.Context.DatabasedSetting;
+            var existingSettings = db.Context.DatabasedSetting.ToList();
 
             // previous entries in EF are removed post migration.
             if (!existingSettings.Any())
                 return;
 
+            Logger.Log("Beginning settings migration to realm", LoggingTarget.Database);
+
             using (var realm = realmContextFactory.CreateContext())
             using (var transaction = realm.BeginWrite())
             {
                 // only migrate data if the realm database is empty.
                 if (!realm.All<RealmRulesetSetting>().Any())
                 {
+                    Logger.Log($"Migrating {existingSettings.Count} settings", LoggingTarget.Database);
+
                     foreach (var dkb in existingSettings)
                     {
                         if (dkb.RulesetID == null)
@@ -136,8 +371,6 @@ namespace osu.Game.Database
                     }
                 }
 
-                db.Context.RemoveRange(existingSettings);
-
                 transaction.Commit();
             }
         }
diff --git a/osu.Game/Database/EntityFrameworkLive.cs b/osu.Game/Database/EntityFrameworkLive.cs
deleted file mode 100644
index 25c0778746..0000000000
--- a/osu.Game/Database/EntityFrameworkLive.cs
+++ /dev/null
@@ -1,41 +0,0 @@
-// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using System;
-
-#nullable enable
-
-namespace osu.Game.Database
-{
-    public class EntityFrameworkLive<T> : ILive<T> where T : class
-    {
-        public EntityFrameworkLive(T item)
-        {
-            IsManaged = true; // no way to really know.
-            Value = item;
-        }
-
-        public Guid ID => throw new InvalidOperationException();
-
-        public void PerformRead(Action<T> perform)
-        {
-            perform(Value);
-        }
-
-        public TReturn PerformRead<TReturn>(Func<T, TReturn> perform)
-        {
-            return perform(Value);
-        }
-
-        public void PerformWrite(Action<T> perform)
-        {
-            perform(Value);
-        }
-
-        public bool IsManaged { get; }
-
-        public T Value { get; }
-
-        public bool Equals(ILive<T>? other) => ID == other?.ID;
-    }
-}
diff --git a/osu.Game/Database/EntityFrameworkLiveExtensions.cs b/osu.Game/Database/EntityFrameworkLiveExtensions.cs
deleted file mode 100644
index cd0673675e..0000000000
--- a/osu.Game/Database/EntityFrameworkLiveExtensions.cs
+++ /dev/null
@@ -1,14 +0,0 @@
-// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-namespace osu.Game.Database
-{
-    public static class EntityFrameworkLiveExtensions
-    {
-        public static ILive<T> ToEntityFrameworkLive<T>(this T item)
-            where T : class
-        {
-            return new EntityFrameworkLive<T>(item);
-        }
-    }
-}
diff --git a/osu.Game/Database/IModelManager.cs b/osu.Game/Database/IModelManager.cs
index 779d0522f7..187ac86a59 100644
--- a/osu.Game/Database/IModelManager.cs
+++ b/osu.Game/Database/IModelManager.cs
@@ -1,7 +1,6 @@
 // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
 // See the LICENCE file in the repository root for full licence text.
 
-using System;
 using System.Collections.Generic;
 
 namespace osu.Game.Database
@@ -13,23 +12,6 @@ namespace osu.Game.Database
     public interface IModelManager<TModel>
         where TModel : class
     {
-        /// <summary>
-        /// Fired when an item is updated.
-        /// </summary>
-        event Action<TModel> ItemUpdated;
-
-        /// <summary>
-        /// Fired when an item is removed.
-        /// </summary>
-        event Action<TModel> ItemRemoved;
-
-        /// <summary>
-        /// Perform an update of the specified item.
-        /// TODO: Support file additions/removals.
-        /// </summary>
-        /// <param name="item">The item to update.</param>
-        void Update(TModel item);
-
         /// <summary>
         /// Delete an item from the manager.
         /// Is a no-op for already deleted items.
diff --git a/osu.Game/Database/ImportTask.cs b/osu.Game/Database/ImportTask.cs
index 1fb5a42630..cd9e396d13 100644
--- a/osu.Game/Database/ImportTask.cs
+++ b/osu.Game/Database/ImportTask.cs
@@ -5,13 +5,14 @@
 
 using System.IO;
 using osu.Game.IO.Archives;
+using osu.Game.Stores;
 using osu.Game.Utils;
 using SharpCompress.Common;
 
 namespace osu.Game.Database
 {
     /// <summary>
-    /// An encapsulated import task to be imported to an <see cref="ArchiveModelManager{TModel,TFileModel}"/>.
+    /// An encapsulated import task to be imported to an <see cref="RealmArchiveModelManager{TModel}"/>.
     /// </summary>
     public class ImportTask
     {
diff --git a/osu.Game/Database/LegacyExporter.cs b/osu.Game/Database/LegacyExporter.cs
index 802ccec6ed..ee960b6b30 100644
--- a/osu.Game/Database/LegacyExporter.cs
+++ b/osu.Game/Database/LegacyExporter.cs
@@ -35,7 +35,7 @@ namespace osu.Game.Database
         /// <param name="item">The item to export.</param>
         public void Export(TModel item)
         {
-            string filename = $"{item.ToString().GetValidArchiveContentFilename()}{FileExtension}";
+            string filename = $"{item.GetDisplayString().GetValidArchiveContentFilename()}{FileExtension}";
 
             using (var stream = exportStorage.GetStream(filename, FileAccess.Write, FileMode.Create))
                 ExportModelTo(item, stream);
diff --git a/osu.Game/Database/LegacyScoreExporter.cs b/osu.Game/Database/LegacyScoreExporter.cs
index 41f8516880..336f50bc3d 100644
--- a/osu.Game/Database/LegacyScoreExporter.cs
+++ b/osu.Game/Database/LegacyScoreExporter.cs
@@ -24,7 +24,7 @@ namespace osu.Game.Database
             if (file == null)
                 return;
 
-            using (var inputStream = UserFileStorage.GetStream(file.FileInfo.GetStoragePath()))
+            using (var inputStream = UserFileStorage.GetStream(file.File.GetStoragePath()))
                 inputStream.CopyTo(outputStream);
         }
     }
diff --git a/osu.Game/Database/ModelDownloader.cs b/osu.Game/Database/ModelDownloader.cs
index 362bc68cc1..2fa3357b06 100644
--- a/osu.Game/Database/ModelDownloader.cs
+++ b/osu.Game/Database/ModelDownloader.cs
@@ -14,7 +14,7 @@ using osu.Game.Overlays.Notifications;
 namespace osu.Game.Database
 {
     public abstract class ModelDownloader<TModel, T> : IModelDownloader<T>
-        where TModel : class, IHasPrimaryKey, ISoftDelete, IEquatable<TModel>, T
+        where TModel : class, IHasGuidPrimaryKey, ISoftDelete, IEquatable<TModel>, T
         where T : class
     {
         public Action<Notification> PostNotification { protected get; set; }
diff --git a/osu.Game/Database/MutableDatabaseBackedStore.cs b/osu.Game/Database/MutableDatabaseBackedStore.cs
deleted file mode 100644
index b0feb7bb78..0000000000
--- a/osu.Game/Database/MutableDatabaseBackedStore.cs
+++ /dev/null
@@ -1,161 +0,0 @@
-// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Linq.Expressions;
-using osu.Framework.Platform;
-
-namespace osu.Game.Database
-{
-    /// <summary>
-    /// A typed store which supports basic addition, deletion and updating for soft-deletable models.
-    /// </summary>
-    /// <typeparam name="T">The databased model.</typeparam>
-    public abstract class MutableDatabaseBackedStore<T> : DatabaseBackedStore
-        where T : class, IHasPrimaryKey, ISoftDelete
-    {
-        /// <summary>
-        /// Fired when an item was added or updated.
-        /// </summary>
-        public event Action<T> ItemUpdated;
-
-        /// <summary>
-        /// Fired when an item was removed.
-        /// </summary>
-        public event Action<T> ItemRemoved;
-
-        protected MutableDatabaseBackedStore(IDatabaseContextFactory contextFactory, Storage storage = null)
-            : base(contextFactory, storage)
-        {
-        }
-
-        /// <summary>
-        /// Access items pre-populated with includes for consumption.
-        /// </summary>
-        public IQueryable<T> ConsumableItems => AddIncludesForConsumption(ContextFactory.Get().Set<T>());
-
-        /// <summary>
-        /// Access barebones items with no includes.
-        /// </summary>
-        public IQueryable<T> Items => ContextFactory.Get().Set<T>();
-
-        /// <summary>
-        /// Add a <typeparamref name="T"/> to the database.
-        /// </summary>
-        /// <param name="item">The item to add.</param>
-        public void Add(T item)
-        {
-            using (var usage = ContextFactory.GetForWrite())
-            {
-                var context = usage.Context;
-                context.Attach(item);
-            }
-
-            ItemUpdated?.Invoke(item);
-        }
-
-        /// <summary>
-        /// Update a <typeparamref name="T"/> in the database.
-        /// </summary>
-        /// <param name="item">The item to update.</param>
-        public void Update(T item)
-        {
-            using (var usage = ContextFactory.GetForWrite())
-                usage.Context.Update(item);
-
-            ItemUpdated?.Invoke(item);
-        }
-
-        /// <summary>
-        /// Delete a <typeparamref name="T"/> from the database.
-        /// </summary>
-        /// <param name="item">The item to delete.</param>
-        public bool Delete(T item)
-        {
-            using (ContextFactory.GetForWrite())
-            {
-                Refresh(ref item);
-
-                if (item.DeletePending) return false;
-
-                item.DeletePending = true;
-            }
-
-            ItemRemoved?.Invoke(item);
-            return true;
-        }
-
-        /// <summary>
-        /// Restore a <typeparamref name="T"/> from a deleted state.
-        /// </summary>
-        /// <param name="item">The item to undelete.</param>
-        public bool Undelete(T item)
-        {
-            using (ContextFactory.GetForWrite())
-            {
-                Refresh(ref item, ConsumableItems);
-
-                if (!item.DeletePending) return false;
-
-                item.DeletePending = false;
-            }
-
-            ItemUpdated?.Invoke(item);
-            return true;
-        }
-
-        /// <summary>
-        /// Allow implementations to add database-side includes or constraints when querying for consumption of items.
-        /// </summary>
-        /// <param name="query">The input query.</param>
-        /// <returns>A potentially modified output query.</returns>
-        protected virtual IQueryable<T> AddIncludesForConsumption(IQueryable<T> query) => query;
-
-        /// <summary>
-        /// Allow implementations to add database-side includes or constraints when deleting items.
-        /// Included properties could then be subsequently deleted by overriding <see cref="Purge"/>.
-        /// </summary>
-        /// <param name="query">The input query.</param>
-        /// <returns>A potentially modified output query.</returns>
-        protected virtual IQueryable<T> AddIncludesForDeletion(IQueryable<T> query) => query;
-
-        /// <summary>
-        /// Called when removing an item completely from the database.
-        /// </summary>
-        /// <param name="items">The items to be purged.</param>
-        /// <param name="context">The write context which can be used to perform subsequent deletions.</param>
-        protected virtual void Purge(List<T> items, OsuDbContext context) => context.RemoveRange(items);
-
-        public override void Cleanup()
-        {
-            base.Cleanup();
-            PurgeDeletable();
-        }
-
-        /// <summary>
-        /// Purge items in a pending delete state.
-        /// </summary>
-        /// <param name="query">An optional query limiting the scope of the purge.</param>
-        public void PurgeDeletable(Expression<Func<T, bool>> query = null)
-        {
-            using (var usage = ContextFactory.GetForWrite())
-            {
-                var context = usage.Context;
-
-                var lookup = context.Set<T>().Where(s => s.DeletePending);
-
-                if (query != null) lookup = lookup.Where(query);
-
-                lookup = AddIncludesForDeletion(lookup);
-
-                var purgeable = lookup.ToList();
-
-                if (!purgeable.Any()) return;
-
-                Purge(purgeable, context);
-            }
-        }
-    }
-}
diff --git a/osu.Game/Database/MutableDatabaseBackedStoreWithFileIncludes.cs b/osu.Game/Database/MutableDatabaseBackedStoreWithFileIncludes.cs
deleted file mode 100644
index 102081cd65..0000000000
--- a/osu.Game/Database/MutableDatabaseBackedStoreWithFileIncludes.cs
+++ /dev/null
@@ -1,27 +0,0 @@
-// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using System.Linq;
-using Microsoft.EntityFrameworkCore;
-using osu.Framework.Platform;
-
-namespace osu.Game.Database
-{
-    public abstract class MutableDatabaseBackedStoreWithFileIncludes<T, TFileInfo> : MutableDatabaseBackedStore<T>
-        where T : class, IHasPrimaryKey, ISoftDelete, IHasFiles<TFileInfo>
-        where TFileInfo : INamedFileInfo
-    {
-        protected MutableDatabaseBackedStoreWithFileIncludes(IDatabaseContextFactory contextFactory, Storage storage = null)
-            : base(contextFactory, storage)
-        {
-        }
-
-        protected override IQueryable<T> AddIncludesForConsumption(IQueryable<T> query) =>
-            base.AddIncludesForConsumption(query)
-                .Include(s => s.Files).ThenInclude(f => f.FileInfo);
-
-        protected override IQueryable<T> AddIncludesForDeletion(IQueryable<T> query) =>
-            base.AddIncludesForDeletion(query)
-                .Include(s => s.Files); // don't include FileInfo. these are handled by the FileStore itself.
-    }
-}
diff --git a/osu.Game/Database/OsuDbContext.cs b/osu.Game/Database/OsuDbContext.cs
index 7cd9ae2885..441b090a6e 100644
--- a/osu.Game/Database/OsuDbContext.cs
+++ b/osu.Game/Database/OsuDbContext.cs
@@ -19,14 +19,14 @@ namespace osu.Game.Database
 {
     public class OsuDbContext : DbContext
     {
-        public DbSet<BeatmapInfo> BeatmapInfo { get; set; }
-        public DbSet<BeatmapDifficulty> BeatmapDifficulty { get; set; }
-        public DbSet<BeatmapMetadata> BeatmapMetadata { get; set; }
-        public DbSet<BeatmapSetInfo> BeatmapSetInfo { get; set; }
+        public DbSet<EFBeatmapInfo> EFBeatmapInfo { get; set; }
+        public DbSet<EFBeatmapDifficulty> BeatmapDifficulty { get; set; }
+        public DbSet<EFBeatmapMetadata> BeatmapMetadata { get; set; }
+        public DbSet<EFBeatmapSetInfo> EFBeatmapSetInfo { get; set; }
         public DbSet<FileInfo> FileInfo { get; set; }
-        public DbSet<RulesetInfo> RulesetInfo { get; set; }
+        public DbSet<EFRulesetInfo> RulesetInfo { get; set; }
         public DbSet<EFSkinInfo> SkinInfo { get; set; }
-        public DbSet<ScoreInfo> ScoreInfo { get; set; }
+        public DbSet<EFScoreInfo> ScoreInfo { get; set; }
 
         // migrated to realm
         public DbSet<DatabasedSetting> DatabasedSetting { get; set; }
@@ -125,13 +125,13 @@ namespace osu.Game.Database
         {
             base.OnModelCreating(modelBuilder);
 
-            modelBuilder.Entity<BeatmapInfo>().HasIndex(b => b.OnlineID).IsUnique();
-            modelBuilder.Entity<BeatmapInfo>().HasIndex(b => b.MD5Hash);
-            modelBuilder.Entity<BeatmapInfo>().HasIndex(b => b.Hash);
+            modelBuilder.Entity<EFBeatmapInfo>().HasIndex(b => b.OnlineID).IsUnique();
+            modelBuilder.Entity<EFBeatmapInfo>().HasIndex(b => b.MD5Hash);
+            modelBuilder.Entity<EFBeatmapInfo>().HasIndex(b => b.Hash);
 
-            modelBuilder.Entity<BeatmapSetInfo>().HasIndex(b => b.OnlineID).IsUnique();
-            modelBuilder.Entity<BeatmapSetInfo>().HasIndex(b => b.DeletePending);
-            modelBuilder.Entity<BeatmapSetInfo>().HasIndex(b => b.Hash).IsUnique();
+            modelBuilder.Entity<EFBeatmapSetInfo>().HasIndex(b => b.OnlineID).IsUnique();
+            modelBuilder.Entity<EFBeatmapSetInfo>().HasIndex(b => b.DeletePending);
+            modelBuilder.Entity<EFBeatmapSetInfo>().HasIndex(b => b.Hash).IsUnique();
 
             modelBuilder.Entity<EFSkinInfo>().HasIndex(b => b.Hash).IsUnique();
             modelBuilder.Entity<EFSkinInfo>().HasIndex(b => b.DeletePending);
@@ -142,12 +142,12 @@ namespace osu.Game.Database
             modelBuilder.Entity<FileInfo>().HasIndex(b => b.Hash).IsUnique();
             modelBuilder.Entity<FileInfo>().HasIndex(b => b.ReferenceCount);
 
-            modelBuilder.Entity<RulesetInfo>().HasIndex(b => b.Available);
-            modelBuilder.Entity<RulesetInfo>().HasIndex(b => b.ShortName).IsUnique();
+            modelBuilder.Entity<EFRulesetInfo>().HasIndex(b => b.Available);
+            modelBuilder.Entity<EFRulesetInfo>().HasIndex(b => b.ShortName).IsUnique();
 
-            modelBuilder.Entity<BeatmapInfo>().HasOne(b => b.BaseDifficulty);
+            modelBuilder.Entity<EFBeatmapInfo>().HasOne(b => b.BaseDifficulty);
 
-            modelBuilder.Entity<ScoreInfo>().HasIndex(b => b.OnlineID).IsUnique();
+            modelBuilder.Entity<EFScoreInfo>().HasIndex(b => b.OnlineID).IsUnique();
         }
 
         private class OsuDbLoggerFactory : ILoggerFactory
diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs
index 96c24837a1..99b357710e 100644
--- a/osu.Game/Database/RealmContextFactory.cs
+++ b/osu.Game/Database/RealmContextFactory.cs
@@ -2,6 +2,7 @@
 // See the LICENCE file in the repository root for full licence text.
 
 using System;
+using System.IO;
 using System.Linq;
 using System.Reflection;
 using System.Threading;
@@ -12,11 +13,15 @@ using osu.Framework.Logging;
 using osu.Framework.Platform;
 using osu.Framework.Statistics;
 using osu.Game.Configuration;
+using osu.Game.Beatmaps;
 using osu.Game.Input.Bindings;
 using osu.Game.Models;
 using osu.Game.Skinning;
 using osu.Game.Stores;
+using osu.Game.Rulesets;
+using osu.Game.Scoring;
 using Realms;
+using Realms.Exceptions;
 
 #nullable enable
 
@@ -45,8 +50,9 @@ namespace osu.Game.Database
         /// 10   2021-11-22    Use ShortName instead of RulesetID for ruleset settings.
         /// 11   2021-11-22    Use ShortName instead of RulesetID for ruleset key bindings.
         /// 12   2021-11-24    Add Status to RealmBeatmapSet.
+        /// 13   2022-01-13    Final migration of beatmaps and scores to realm (multiple new storage fields).
         /// </summary>
-        private const int schema_version = 12;
+        private const int schema_version = 13;
 
         /// <summary>
         /// Lock object which is held during <see cref="BlockAllOperations"/> sections, blocking context creation during blocking periods.
@@ -100,8 +106,20 @@ namespace osu.Game.Database
             if (!Filename.EndsWith(realm_extension, StringComparison.Ordinal))
                 Filename += realm_extension;
 
-            // This method triggers the first `CreateContext` call, which will implicitly run realm migrations and bring the schema up-to-date.
-            cleanupPendingDeletions();
+            try
+            {
+                // This method triggers the first `CreateContext` call, which will implicitly run realm migrations and bring the schema up-to-date.
+                cleanupPendingDeletions();
+            }
+            catch (Exception e)
+            {
+                Logger.Error(e, "Realm startup failed with unrecoverable error; starting with a fresh database. A backup of your database has been made.");
+
+                CreateBackup($"{Filename.Replace(realm_extension, string.Empty)}_{DateTimeOffset.UtcNow.ToUnixTimeSeconds()}_corrupt{realm_extension}");
+                storage.Delete(Filename);
+
+                cleanupPendingDeletions();
+            }
         }
 
         private void cleanupPendingDeletions()
@@ -109,14 +127,27 @@ namespace osu.Game.Database
             using (var realm = CreateContext())
             using (var transaction = realm.BeginWrite())
             {
-                var pendingDeleteSets = realm.All<RealmBeatmapSet>().Where(s => s.DeletePending);
+                var pendingDeleteScores = realm.All<ScoreInfo>().Where(s => s.DeletePending);
 
-                foreach (var s in pendingDeleteSets)
+                foreach (var score in pendingDeleteScores)
+                    realm.Remove(score);
+
+                var pendingDeleteSets = realm.All<BeatmapSetInfo>().Where(s => s.DeletePending);
+
+                foreach (var beatmapSet in pendingDeleteSets)
                 {
-                    foreach (var b in s.Beatmaps)
-                        realm.Remove(b);
+                    foreach (var beatmap in beatmapSet.Beatmaps)
+                    {
+                        // Cascade delete related scores, else they will have a null beatmap against the model's spec.
+                        foreach (var score in beatmap.Scores)
+                            realm.Remove(score);
 
-                    realm.Remove(s);
+                        realm.Remove(beatmap.Metadata);
+
+                        realm.Remove(beatmap);
+                    }
+
+                    realm.Remove(beatmapSet);
                 }
 
                 var pendingDeleteSkins = realm.All<SkinInfo>().Where(s => s.DeletePending);
@@ -188,10 +219,17 @@ namespace osu.Game.Database
 
         private RealmConfiguration getConfiguration()
         {
+            // This is currently the only usage of temporary files at the osu! side.
+            // If we use the temporary folder in more situations in the future, this should be moved to a higher level (helper method or OsuGameBase).
+            string tempPathLocation = Path.Combine(Path.GetTempPath(), @"lazer");
+            if (!Directory.Exists(tempPathLocation))
+                Directory.CreateDirectory(tempPathLocation);
+
             return new RealmConfiguration(storage.GetFullPath(Filename, true))
             {
                 SchemaVersion = schema_version,
                 MigrationCallback = onMigration,
+                FallbackPipePath = tempPathLocation,
             };
         }
 
@@ -206,9 +244,9 @@ namespace osu.Game.Database
             switch (targetVersion)
             {
                 case 7:
-                    convertOnlineIDs<RealmBeatmap>();
-                    convertOnlineIDs<RealmBeatmapSet>();
-                    convertOnlineIDs<RealmRuleset>();
+                    convertOnlineIDs<BeatmapInfo>();
+                    convertOnlineIDs<BeatmapSetInfo>();
+                    convertOnlineIDs<RulesetInfo>();
 
                     void convertOnlineIDs<T>() where T : RealmObject
                     {
@@ -253,14 +291,14 @@ namespace osu.Game.Database
 
                 case 9:
                     // Pretty pointless to do this as beatmaps aren't really loaded via realm yet, but oh well.
-                    string metadataClassName = getMappedOrOriginalName(typeof(RealmBeatmapMetadata));
+                    string metadataClassName = getMappedOrOriginalName(typeof(BeatmapMetadata));
 
                     // May be coming from a version before `RealmBeatmapMetadata` existed.
                     if (!migration.OldRealm.Schema.TryFindObjectSchema(metadataClassName, out _))
                         return;
 
                     var oldMetadata = migration.OldRealm.DynamicApi.All(metadataClassName);
-                    var newMetadata = migration.NewRealm.All<RealmBeatmapMetadata>();
+                    var newMetadata = migration.NewRealm.All<BeatmapMetadata>();
 
                     int metadataCount = newMetadata.Count();
 
@@ -336,6 +374,17 @@ namespace osu.Game.Database
         private string? getRulesetShortNameFromLegacyID(long rulesetId) =>
             efContextFactory?.Get().RulesetInfo.FirstOrDefault(r => r.ID == rulesetId)?.ShortName;
 
+        public void CreateBackup(string backupFilename)
+        {
+            using (BlockAllOperations())
+            {
+                Logger.Log($"Creating full realm database backup at {backupFilename}", LoggingTarget.Database);
+                using (var source = storage.GetStream(Filename))
+                using (var destination = storage.GetStream(backupFilename, FileAccess.Write, FileMode.CreateNew))
+                    source.CopyTo(destination);
+            }
+        }
+
         /// <summary>
         /// Flush any active contexts and block any further writes.
         /// </summary>
@@ -349,17 +398,17 @@ namespace osu.Game.Database
             if (isDisposed)
                 throw new ObjectDisposedException(nameof(RealmContextFactory));
 
-            if (!ThreadSafety.IsUpdateThread)
-                throw new InvalidOperationException(@$"{nameof(BlockAllOperations)} must be called from the update thread.");
-
-            Logger.Log(@"Blocking realm operations.", LoggingTarget.Database);
-
             try
             {
                 contextCreationLock.Wait();
 
                 lock (contextLock)
                 {
+                    if (!ThreadSafety.IsUpdateThread && context != null)
+                        throw new InvalidOperationException(@$"{nameof(BlockAllOperations)} must be called from the update thread.");
+
+                    Logger.Log(@"Blocking realm operations.", LoggingTarget.Database);
+
                     context?.Dispose();
                     context = null;
                 }
@@ -367,14 +416,23 @@ namespace osu.Game.Database
                 const int sleep_length = 200;
                 int timeout = 5000;
 
-                // see https://github.com/realm/realm-dotnet/discussions/2657
-                while (!Compact())
+                try
                 {
-                    Thread.Sleep(sleep_length);
-                    timeout -= sleep_length;
+                    // see https://github.com/realm/realm-dotnet/discussions/2657
+                    while (!Compact())
+                    {
+                        Thread.Sleep(sleep_length);
+                        timeout -= sleep_length;
 
-                    if (timeout < 0)
-                        throw new TimeoutException(@"Took too long to acquire lock");
+                        if (timeout < 0)
+                            throw new TimeoutException(@"Took too long to acquire lock");
+                    }
+                }
+                catch (RealmException e)
+                {
+                    // Compact may fail if the realm is in a bad state.
+                    // We still want to continue with the blocking operation, though.
+                    Logger.Log($"Realm compact failed with error {e}", LoggingTarget.Database);
                 }
             }
             catch
diff --git a/osu.Game/Database/RealmLive.cs b/osu.Game/Database/RealmLive.cs
index 90b8814c24..6594224666 100644
--- a/osu.Game/Database/RealmLive.cs
+++ b/osu.Game/Database/RealmLive.cs
@@ -61,14 +61,18 @@ namespace osu.Game.Database
         /// <param name="perform">The action to perform.</param>
         public TReturn PerformRead<TReturn>(Func<T, TReturn> perform)
         {
-            if (typeof(RealmObjectBase).IsAssignableFrom(typeof(TReturn)))
-                throw new InvalidOperationException(@$"Realm live objects should not exit the scope of {nameof(PerformRead)}.");
-
             if (!IsManaged)
                 return perform(data);
 
             using (var realm = realmFactory.CreateContext())
-                return perform(realm.Find<T>(ID));
+            {
+                var returnData = perform(realm.Find<T>(ID));
+
+                if (returnData is RealmObjectBase realmObject && realmObject.IsManaged)
+                    throw new InvalidOperationException(@$"Managed realm objects should not exit the scope of {nameof(PerformRead)}.");
+
+                return returnData;
+            }
         }
 
         /// <summary>
diff --git a/osu.Game/Database/RealmObjectExtensions.cs b/osu.Game/Database/RealmObjectExtensions.cs
index e09f046421..746a43fd37 100644
--- a/osu.Game/Database/RealmObjectExtensions.cs
+++ b/osu.Game/Database/RealmObjectExtensions.cs
@@ -4,9 +4,15 @@
 using System;
 using System.Collections.Generic;
 using System.Linq;
+using System.Runtime.Serialization;
 using AutoMapper;
+using AutoMapper.Internal;
 using osu.Framework.Development;
+using osu.Game.Beatmaps;
 using osu.Game.Input.Bindings;
+using osu.Game.Models;
+using osu.Game.Rulesets;
+using osu.Game.Scoring;
 using Realms;
 
 #nullable enable
@@ -15,13 +21,135 @@ namespace osu.Game.Database
 {
     public static class RealmObjectExtensions
     {
-        private static readonly IMapper mapper = new MapperConfiguration(c =>
+        private static readonly IMapper write_mapper = new MapperConfiguration(c =>
         {
             c.ShouldMapField = fi => false;
-            c.ShouldMapProperty = pi => pi.SetMethod != null && pi.SetMethod.IsPublic;
+            c.ShouldMapProperty = pi => pi.SetMethod?.IsPublic == true;
+
+            c.CreateMap<BeatmapMetadata, BeatmapMetadata>()
+             .ForMember(s => s.Author, cc => cc.Ignore())
+             .AfterMap((s, d) =>
+             {
+                 copyChangesToRealm(s.Author, d.Author);
+             });
+            c.CreateMap<BeatmapDifficulty, BeatmapDifficulty>();
+            c.CreateMap<RealmUser, RealmUser>();
+            c.CreateMap<RealmFile, RealmFile>();
+            c.CreateMap<RealmNamedFileUsage, RealmNamedFileUsage>();
+            c.CreateMap<BeatmapInfo, BeatmapInfo>()
+             .ForMember(s => s.Ruleset, cc => cc.Ignore())
+             .ForMember(s => s.Metadata, cc => cc.Ignore())
+             .ForMember(s => s.Difficulty, cc => cc.Ignore())
+             .ForMember(s => s.BeatmapSet, cc => cc.Ignore())
+             .AfterMap((s, d) =>
+             {
+                 d.Ruleset = d.Realm.Find<RulesetInfo>(s.Ruleset.ShortName);
+                 copyChangesToRealm(s.Difficulty, d.Difficulty);
+                 copyChangesToRealm(s.Metadata, d.Metadata);
+             });
+            c.CreateMap<BeatmapSetInfo, BeatmapSetInfo>()
+             .ForMember(s => s.Beatmaps, cc => cc.Ignore())
+             .AfterMap((s, d) =>
+             {
+                 foreach (var beatmap in s.Beatmaps)
+                 {
+                     var existing = d.Beatmaps.FirstOrDefault(b => b.ID == beatmap.ID);
+
+                     if (existing != null)
+                         copyChangesToRealm(beatmap, existing);
+                     else
+                         d.Beatmaps.Add(beatmap);
+                 }
+             });
+
+            c.Internal().ForAllMaps((typeMap, expression) =>
+            {
+                expression.ForAllMembers(m =>
+                {
+                    if (m.DestinationMember.Has<IgnoredAttribute>() || m.DestinationMember.Has<BacklinkAttribute>() || m.DestinationMember.Has<IgnoreDataMemberAttribute>())
+                        m.Ignore();
+                });
+            });
+        }).CreateMapper();
+
+        private static readonly IMapper mapper = new MapperConfiguration(c =>
+        {
+            applyCommonConfiguration(c);
+
+            c.CreateMap<BeatmapSetInfo, BeatmapSetInfo>()
+             .MaxDepth(2)
+             .AfterMap((s, d) =>
+             {
+                 foreach (var beatmap in d.Beatmaps)
+                     beatmap.BeatmapSet = d;
+             });
+
+            // This can be further optimised to reduce cyclic retrievals, similar to the optimised set mapper below.
+            // Only hasn't been done yet as we detach at the point of BeatmapInfo less often.
+            c.CreateMap<BeatmapInfo, BeatmapInfo>()
+             .MaxDepth(2)
+             .AfterMap((s, d) =>
+             {
+                 for (int i = 0; i < d.BeatmapSet?.Beatmaps.Count; i++)
+                 {
+                     if (d.BeatmapSet.Beatmaps[i].Equals(d))
+                     {
+                         d.BeatmapSet.Beatmaps[i] = d;
+                         break;
+                     }
+                 }
+             });
+        }).CreateMapper();
+
+        /// <summary>
+        /// A slightly optimised mapper that avoids double-fetches in cyclic reference.
+        /// </summary>
+        private static readonly IMapper beatmap_set_mapper = new MapperConfiguration(c =>
+        {
+            applyCommonConfiguration(c);
+
+            c.CreateMap<BeatmapSetInfo, BeatmapSetInfo>()
+             .MaxDepth(2)
+             .ForMember(b => b.Files, cc => cc.Ignore())
+             .AfterMap((s, d) =>
+             {
+                 foreach (var beatmap in d.Beatmaps)
+                     beatmap.BeatmapSet = d;
+             });
+
+            c.CreateMap<BeatmapInfo, BeatmapInfo>()
+             .MaxDepth(1)
+             // This is not required as it will be populated in the `AfterMap` call from the `BeatmapInfo`'s parent.
+             .ForMember(b => b.BeatmapSet, cc => cc.Ignore());
+        }).CreateMapper();
+
+        private static void applyCommonConfiguration(IMapperConfigurationExpression c)
+        {
+            c.ShouldMapField = fi => false;
+
+            // This is specifically to avoid mapping explicit interface implementations.
+            // If we want to limit this further, we can avoid mapping properties with no setter that are not IList<>.
+            // Takes a bit of effort to determine whether this is the case though, see https://stackoverflow.com/questions/951536/how-do-i-tell-whether-a-type-implements-ilist
+            c.ShouldMapProperty = pi => pi.GetMethod?.IsPublic == true;
+
+            c.Internal().ForAllMaps((typeMap, expression) =>
+            {
+                expression.ForAllMembers(m =>
+                {
+                    if (m.DestinationMember.Has<IgnoredAttribute>() || m.DestinationMember.Has<BacklinkAttribute>() || m.DestinationMember.Has<IgnoreDataMemberAttribute>())
+                        m.Ignore();
+                });
+            });
 
             c.CreateMap<RealmKeyBinding, RealmKeyBinding>();
-        }).CreateMapper();
+            c.CreateMap<BeatmapMetadata, BeatmapMetadata>();
+            c.CreateMap<BeatmapDifficulty, BeatmapDifficulty>();
+            c.CreateMap<RulesetInfo, RulesetInfo>();
+            c.CreateMap<ScoreInfo, ScoreInfo>();
+            c.CreateMap<RealmUser, RealmUser>();
+            c.CreateMap<RealmFile, RealmFile>();
+            c.CreateMap<RealmNamedFileUsage, RealmNamedFileUsage>();
+        }
 
         /// <summary>
         /// Create a detached copy of the each item in the collection.
@@ -32,7 +160,7 @@ namespace osu.Game.Database
         /// <param name="items">A list of managed <see cref="RealmObject"/>s to detach.</param>
         /// <typeparam name="T">The type of object.</typeparam>
         /// <returns>A list containing non-managed copies of provided items.</returns>
-        public static List<T> Detach<T>(this IEnumerable<T> items) where T : RealmObject
+        public static List<T> Detach<T>(this IEnumerable<T> items) where T : RealmObjectBase
         {
             var list = new List<T>();
 
@@ -51,14 +179,29 @@ namespace osu.Game.Database
         /// <param name="item">The managed <see cref="RealmObject"/> to detach.</param>
         /// <typeparam name="T">The type of object.</typeparam>
         /// <returns>A non-managed copy of provided item. Will return the provided item if already detached.</returns>
-        public static T Detach<T>(this T item) where T : RealmObject
+        public static T Detach<T>(this T item) where T : RealmObjectBase
         {
             if (!item.IsManaged)
                 return item;
 
+            if (item is BeatmapSetInfo)
+                return beatmap_set_mapper.Map<T>(item);
+
             return mapper.Map<T>(item);
         }
 
+        /// <summary>
+        /// Copy changes in a detached beatmap back to realm.
+        /// This is a temporary method to handle existing flows only. It should not be used going forward if we can avoid it.
+        /// </summary>
+        /// <param name="source">The detached beatmap to copy from.</param>
+        /// <param name="destination">The live beatmap to copy to.</param>
+        public static void CopyChangesToRealm(this BeatmapSetInfo source, BeatmapSetInfo destination)
+            => copyChangesToRealm(source, destination);
+
+        private static void copyChangesToRealm<T>(T source, T destination) where T : RealmObjectBase
+            => write_mapper.Map(source, destination);
+
         public static List<ILive<T>> ToLiveUnmanaged<T>(this IEnumerable<T> realmList)
             where T : RealmObject, IHasGuidPrimaryKey
         {
diff --git a/osu.Game/Graphics/Sprites/LogoAnimation.cs b/osu.Game/Graphics/Sprites/LogoAnimation.cs
index b1383065fe..36fcd39b54 100644
--- a/osu.Game/Graphics/Sprites/LogoAnimation.cs
+++ b/osu.Game/Graphics/Sprites/LogoAnimation.cs
@@ -7,14 +7,13 @@ using osu.Framework.Graphics;
 using osu.Framework.Graphics.OpenGL.Vertices;
 using osu.Framework.Graphics.Shaders;
 using osu.Framework.Graphics.Sprites;
-using osu.Framework.Graphics.Textures;
 
 namespace osu.Game.Graphics.Sprites
 {
     public class LogoAnimation : Sprite
     {
         [BackgroundDependencyLoader]
-        private void load(ShaderManager shaders, TextureStore textures)
+        private void load(ShaderManager shaders)
         {
             TextureShader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, @"LogoAnimation");
             RoundedTextureShader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, @"LogoAnimation"); // Masking isn't supported for now
diff --git a/osu.Game/Graphics/UserInterface/DrawableOsuMenuItem.cs b/osu.Game/Graphics/UserInterface/DrawableOsuMenuItem.cs
index fea84998cf..4267b82bb7 100644
--- a/osu.Game/Graphics/UserInterface/DrawableOsuMenuItem.cs
+++ b/osu.Game/Graphics/UserInterface/DrawableOsuMenuItem.cs
@@ -2,7 +2,6 @@
 // See the LICENCE file in the repository root for full licence text.
 
 using osu.Framework.Allocation;
-using osu.Framework.Audio;
 using osu.Framework.Extensions.Color4Extensions;
 using osu.Framework.Graphics;
 using osu.Framework.Graphics.Containers;
@@ -30,7 +29,7 @@ namespace osu.Game.Graphics.UserInterface
         }
 
         [BackgroundDependencyLoader]
-        private void load(AudioManager audio)
+        private void load()
         {
             BackgroundColour = Color4.Transparent;
             BackgroundColourHover = Color4Extensions.FromHex(@"172023");
diff --git a/osu.Game/Graphics/UserInterface/HoverSampleDebounceComponent.cs b/osu.Game/Graphics/UserInterface/HoverSampleDebounceComponent.cs
index 1fd03a34e7..34ab7626c9 100644
--- a/osu.Game/Graphics/UserInterface/HoverSampleDebounceComponent.cs
+++ b/osu.Game/Graphics/UserInterface/HoverSampleDebounceComponent.cs
@@ -2,7 +2,6 @@
 // See the LICENCE file in the repository root for full licence text.
 
 using osu.Framework.Allocation;
-using osu.Framework.Audio;
 using osu.Framework.Bindables;
 using osu.Framework.Graphics.Containers;
 using osu.Framework.Input.Events;
@@ -18,7 +17,7 @@ namespace osu.Game.Graphics.UserInterface
         private Bindable<double?> lastPlaybackTime;
 
         [BackgroundDependencyLoader]
-        private void load(AudioManager audio, SessionStatics statics)
+        private void load(SessionStatics statics)
         {
             lastPlaybackTime = statics.GetBindable<double?>(Static.LastHoverSoundPlaybackTime);
         }
diff --git a/osu.Game/Graphics/UserInterface/OsuContextMenu.cs b/osu.Game/Graphics/UserInterface/OsuContextMenu.cs
index cf201b18b4..e0946fd9e1 100644
--- a/osu.Game/Graphics/UserInterface/OsuContextMenu.cs
+++ b/osu.Game/Graphics/UserInterface/OsuContextMenu.cs
@@ -3,7 +3,6 @@
 
 using osuTK.Graphics;
 using osu.Framework.Allocation;
-using osu.Framework.Audio;
 using osu.Framework.Extensions.Color4Extensions;
 using osu.Framework.Graphics;
 using osu.Framework.Graphics.Effects;
@@ -41,7 +40,7 @@ namespace osu.Game.Graphics.UserInterface
         }
 
         [BackgroundDependencyLoader]
-        private void load(OsuColour colours, AudioManager audio)
+        private void load(OsuColour colours)
         {
             BackgroundColour = colours.ContextMenuGray;
         }
diff --git a/osu.Game/Graphics/UserInterface/OsuContextMenuSamples.cs b/osu.Game/Graphics/UserInterface/OsuContextMenuSamples.cs
index d67ea499e5..921fef7951 100644
--- a/osu.Game/Graphics/UserInterface/OsuContextMenuSamples.cs
+++ b/osu.Game/Graphics/UserInterface/OsuContextMenuSamples.cs
@@ -16,7 +16,7 @@ namespace osu.Game.Graphics.UserInterface
         private Sample sampleClose;
 
         [BackgroundDependencyLoader]
-        private void load(OsuColour colours, AudioManager audio)
+        private void load(AudioManager audio)
         {
             sampleClick = audio.Samples.Get($@"UI/{HoverSampleSet.Default.GetDescription()}-select");
             sampleOpen = audio.Samples.Get(@"UI/dropdown-open");
diff --git a/osu.Game/Graphics/UserInterface/OsuNumberBox.cs b/osu.Game/Graphics/UserInterface/OsuNumberBox.cs
index 3d565a4464..8a3b77d3c2 100644
--- a/osu.Game/Graphics/UserInterface/OsuNumberBox.cs
+++ b/osu.Game/Graphics/UserInterface/OsuNumberBox.cs
@@ -7,6 +7,8 @@ namespace osu.Game.Graphics.UserInterface
 {
     public class OsuNumberBox : OsuTextBox
     {
+        protected override bool AllowIme => false;
+
         protected override bool CanAddCharacter(char character) => character.IsAsciiDigit();
     }
 }
diff --git a/osu.Game/Graphics/UserInterface/OsuPasswordTextBox.cs b/osu.Game/Graphics/UserInterface/OsuPasswordTextBox.cs
index 8e82f4a7c1..b276159558 100644
--- a/osu.Game/Graphics/UserInterface/OsuPasswordTextBox.cs
+++ b/osu.Game/Graphics/UserInterface/OsuPasswordTextBox.cs
@@ -29,6 +29,10 @@ namespace osu.Game.Graphics.UserInterface
 
         protected override bool AllowClipboardExport => false;
 
+        protected override bool AllowWordNavigation => false;
+
+        protected override bool AllowIme => false;
+
         private readonly CapsWarning warning;
 
         [Resolved]
diff --git a/osu.Game/IO/FileStore.cs b/osu.Game/IO/FileStore.cs
deleted file mode 100644
index ebe1ebfe69..0000000000
--- a/osu.Game/IO/FileStore.cs
+++ /dev/null
@@ -1,125 +0,0 @@
-// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using System;
-using System.IO;
-using System.Linq;
-using osu.Framework.Extensions;
-using osu.Framework.IO.Stores;
-using osu.Framework.Logging;
-using osu.Framework.Platform;
-using osu.Game.Database;
-using osu.Game.Extensions;
-
-namespace osu.Game.IO
-{
-    /// <summary>
-    /// Handles the Store and retrieval of Files/FileSets to the database backing
-    /// </summary>
-    public class FileStore : DatabaseBackedStore
-    {
-        public readonly IResourceStore<byte[]> Store;
-
-        public new Storage Storage => base.Storage;
-
-        public FileStore(IDatabaseContextFactory contextFactory, Storage storage)
-            : base(contextFactory, storage.GetStorageForDirectory(@"files"))
-        {
-            Store = new StorageBackedResourceStore(Storage);
-        }
-
-        public FileInfo Add(Stream data, bool reference = true)
-        {
-            using (var usage = ContextFactory.GetForWrite())
-            {
-                string hash = data.ComputeSHA2Hash();
-
-                var existing = usage.Context.FileInfo.FirstOrDefault(f => f.Hash == hash);
-
-                var info = existing ?? new FileInfo { Hash = hash };
-
-                string path = info.GetStoragePath();
-
-                // we may be re-adding a file to fix missing store entries.
-                bool requiresCopy = !Storage.Exists(path);
-
-                if (!requiresCopy)
-                {
-                    // even if the file already exists, check the existing checksum for safety.
-                    using (var stream = Storage.GetStream(path))
-                        requiresCopy |= stream.ComputeSHA2Hash() != hash;
-                }
-
-                if (requiresCopy)
-                {
-                    data.Seek(0, SeekOrigin.Begin);
-
-                    using (var output = Storage.GetStream(path, FileAccess.Write))
-                        data.CopyTo(output);
-
-                    data.Seek(0, SeekOrigin.Begin);
-                }
-
-                if (reference || existing == null)
-                    Reference(info);
-
-                return info;
-            }
-        }
-
-        public void Reference(params FileInfo[] files)
-        {
-            if (files.Length == 0) return;
-
-            using (var usage = ContextFactory.GetForWrite())
-            {
-                var context = usage.Context;
-
-                foreach (var f in files.GroupBy(f => f.ID))
-                {
-                    var refetch = context.Find<FileInfo>(f.First().ID) ?? f.First();
-                    refetch.ReferenceCount += f.Count();
-                    context.FileInfo.Update(refetch);
-                }
-            }
-        }
-
-        public void Dereference(params FileInfo[] files)
-        {
-            if (files.Length == 0) return;
-
-            using (var usage = ContextFactory.GetForWrite())
-            {
-                var context = usage.Context;
-
-                foreach (var f in files.GroupBy(f => f.ID))
-                {
-                    var refetch = context.FileInfo.Find(f.Key);
-                    refetch.ReferenceCount -= f.Count();
-                    context.FileInfo.Update(refetch);
-                }
-            }
-        }
-
-        public override void Cleanup()
-        {
-            using (var usage = ContextFactory.GetForWrite())
-            {
-                var context = usage.Context;
-
-                foreach (var f in context.FileInfo.Where(f => f.ReferenceCount < 1))
-                {
-                    try
-                    {
-                        Storage.Delete(f.GetStoragePath());
-                        context.FileInfo.Remove(f);
-                    }
-                    catch (Exception e)
-                    {
-                        Logger.Error(e, $@"Could not delete beatmap {f}");
-                    }
-                }
-            }
-        }
-    }
-}
diff --git a/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs b/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs
index f95c884fe5..03b069d431 100644
--- a/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs
+++ b/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs
@@ -78,19 +78,20 @@ namespace osu.Game.Input.Bindings
         {
             var defaults = DefaultKeyBindings.ToList();
 
-            if (ruleset != null && !ruleset.ID.HasValue)
-                // some tests instantiate a ruleset which is not present in the database.
-                // in these cases we still want key bindings to work, but matching to database instances would result in none being present,
-                // so let's populate the defaults directly.
+            List<RealmKeyBinding> newBindings = realmKeyBindings.Detach()
+                                                                // this ordering is important to ensure that we read entries from the database in the order
+                                                                // enforced by DefaultKeyBindings. allow for song select to handle actions that may otherwise
+                                                                // have been eaten by the music controller due to query order.
+                                                                .OrderBy(b => defaults.FindIndex(d => (int)d.Action == b.ActionInt)).ToList();
+
+            // In the case no bindings were found in the database, presume this usage is for a non-databased ruleset.
+            // This actually should never be required and can be removed if it is ever deemed to cause a problem.
+            // See https://github.com/ppy/osu/issues/8805 for original reasoning, which is no longer valid as we use ShortName
+            // for lookups these days.
+            if (newBindings.Count == 0)
                 KeyBindings = defaults;
             else
-            {
-                KeyBindings = realmKeyBindings.Detach()
-                                              // this ordering is important to ensure that we read entries from the database in the order
-                                              // enforced by DefaultKeyBindings. allow for song select to handle actions that may otherwise
-                                              // have been eaten by the music controller due to query order.
-                                              .OrderBy(b => defaults.FindIndex(d => (int)d.Action == b.ActionInt)).ToList();
-            }
+                KeyBindings = newBindings;
         }
     }
 }
diff --git a/osu.Game/Input/Bindings/RealmKeyBinding.cs b/osu.Game/Input/Bindings/RealmKeyBinding.cs
index 6a408847fe..32813ada16 100644
--- a/osu.Game/Input/Bindings/RealmKeyBinding.cs
+++ b/osu.Game/Input/Bindings/RealmKeyBinding.cs
@@ -20,12 +20,14 @@ namespace osu.Game.Input.Bindings
 
         public int? Variant { get; set; }
 
+        [Ignored]
         public KeyCombination KeyCombination
         {
             get => KeyCombinationString;
             set => KeyCombinationString = value.ToString();
         }
 
+        [Ignored]
         public object Action
         {
             get => ActionInt;
diff --git a/osu.Game/Migrations/20171019041408_InitialCreate.Designer.cs b/osu.Game/Migrations/20171019041408_InitialCreate.Designer.cs
deleted file mode 100644
index c751530bf4..0000000000
--- a/osu.Game/Migrations/20171019041408_InitialCreate.Designer.cs
+++ /dev/null
@@ -1,293 +0,0 @@
-// <auto-generated />
-using Microsoft.EntityFrameworkCore;
-using Microsoft.EntityFrameworkCore.Infrastructure;
-using Microsoft.EntityFrameworkCore.Metadata;
-using Microsoft.EntityFrameworkCore.Migrations;
-using Microsoft.EntityFrameworkCore.Storage;
-using osu.Game.Database;
-using System;
-
-namespace osu.Game.Migrations
-{
-    [DbContext(typeof(OsuDbContext))]
-    [Migration("20171019041408_InitialCreate")]
-    partial class InitialCreate
-    {
-        protected override void BuildTargetModel(ModelBuilder modelBuilder)
-        {
-#pragma warning disable 612, 618
-            modelBuilder
-                .HasAnnotation("ProductVersion", "2.0.0-rtm-26452");
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapDifficulty", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<float>("ApproachRate");
-
-                    b.Property<float>("CircleSize");
-
-                    b.Property<float>("DrainRate");
-
-                    b.Property<float>("OverallDifficulty");
-
-                    b.Property<float>("SliderMultiplier");
-
-                    b.Property<float>("SliderTickRate");
-
-                    b.HasKey("ID");
-
-                    b.ToTable("BeatmapDifficulty");
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<int>("AudioLeadIn");
-
-                    b.Property<int>("BaseDifficultyID");
-
-                    b.Property<int>("BeatDivisor");
-
-                    b.Property<int>("BeatmapSetInfoID");
-
-                    b.Property<bool>("Countdown");
-
-                    b.Property<double>("DistanceSpacing");
-
-                    b.Property<int>("GridSize");
-
-                    b.Property<string>("Hash");
-
-                    b.Property<bool>("Hidden");
-
-                    b.Property<bool>("LetterboxInBreaks");
-
-                    b.Property<string>("MD5Hash");
-
-                    b.Property<int?>("MetadataID");
-
-                    b.Property<int?>("OnlineBeatmapID");
-
-                    b.Property<string>("Path");
-
-                    b.Property<int>("RulesetID");
-
-                    b.Property<bool>("SpecialStyle");
-
-                    b.Property<float>("StackLeniency");
-
-                    b.Property<double>("StarDifficulty");
-
-                    b.Property<string>("StoredBookmarks");
-
-                    b.Property<double>("TimelineZoom");
-
-                    b.Property<string>("Version");
-
-                    b.Property<bool>("WidescreenStoryboard");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("BaseDifficultyID");
-
-                    b.HasIndex("BeatmapSetInfoID");
-
-                    b.HasIndex("Hash");
-
-                    b.HasIndex("MD5Hash");
-
-                    b.HasIndex("MetadataID");
-
-                    b.HasIndex("RulesetID");
-
-                    b.ToTable("BeatmapInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapMetadata", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<string>("Artist");
-
-                    b.Property<string>("ArtistUnicode");
-
-                    b.Property<string>("AudioFile");
-
-                    b.Property<string>("AuthorString")
-                        .HasColumnName("Author");
-
-                    b.Property<string>("BackgroundFile");
-
-                    b.Property<int>("PreviewTime");
-
-                    b.Property<string>("Source");
-
-                    b.Property<string>("Tags");
-
-                    b.Property<string>("Title");
-
-                    b.Property<string>("TitleUnicode");
-
-                    b.HasKey("ID");
-
-                    b.ToTable("BeatmapMetadata");
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<int>("BeatmapSetInfoID");
-
-                    b.Property<int>("FileInfoID");
-
-                    b.Property<string>("Filename")
-                        .IsRequired();
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("BeatmapSetInfoID");
-
-                    b.HasIndex("FileInfoID");
-
-                    b.ToTable("BeatmapSetFileInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<bool>("DeletePending");
-
-                    b.Property<string>("Hash");
-
-                    b.Property<int?>("MetadataID");
-
-                    b.Property<int?>("OnlineBeatmapSetID");
-
-                    b.Property<bool>("Protected");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("DeletePending");
-
-                    b.HasIndex("Hash");
-
-                    b.HasIndex("MetadataID");
-
-                    b.ToTable("BeatmapSetInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Input.Bindings.DatabasedKeyBinding", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<int>("IntAction")
-                        .HasColumnName("Action");
-
-                    b.Property<string>("KeysString")
-                        .HasColumnName("Keys");
-
-                    b.Property<int?>("RulesetID");
-
-                    b.Property<int?>("Variant");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("IntAction");
-
-                    b.HasIndex("Variant");
-
-                    b.ToTable("KeyBinding");
-                });
-
-            modelBuilder.Entity("osu.Game.IO.FileInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<string>("Hash");
-
-                    b.Property<int>("ReferenceCount");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("Hash")
-                        .IsUnique();
-
-                    b.HasIndex("ReferenceCount");
-
-                    b.ToTable("FileInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Rulesets.RulesetInfo", b =>
-                {
-                    b.Property<int?>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<bool>("Available");
-
-                    b.Property<string>("InstantiationInfo");
-
-                    b.Property<string>("Name");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("Available");
-
-                    b.ToTable("RulesetInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b =>
-                {
-                    b.HasOne("osu.Game.Beatmaps.BeatmapDifficulty", "BaseDifficulty")
-                        .WithMany()
-                        .HasForeignKey("BaseDifficultyID")
-                        .OnDelete(DeleteBehavior.Cascade);
-
-                    b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo", "BeatmapSet")
-                        .WithMany("Beatmaps")
-                        .HasForeignKey("BeatmapSetInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-
-                    b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata")
-                        .WithMany("Beatmaps")
-                        .HasForeignKey("MetadataID");
-
-                    b.HasOne("osu.Game.Rulesets.RulesetInfo", "Ruleset")
-                        .WithMany()
-                        .HasForeignKey("RulesetID")
-                        .OnDelete(DeleteBehavior.Cascade);
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b =>
-                {
-                    b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo")
-                        .WithMany("Files")
-                        .HasForeignKey("BeatmapSetInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-
-                    b.HasOne("osu.Game.IO.FileInfo", "FileInfo")
-                        .WithMany()
-                        .HasForeignKey("FileInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b =>
-                {
-                    b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata")
-                        .WithMany("BeatmapSets")
-                        .HasForeignKey("MetadataID");
-                });
-#pragma warning restore 612, 618
-        }
-    }
-}
diff --git a/osu.Game/Migrations/20171019041408_InitialCreate.cs b/osu.Game/Migrations/20171019041408_InitialCreate.cs
deleted file mode 100644
index 9b6881f98c..0000000000
--- a/osu.Game/Migrations/20171019041408_InitialCreate.cs
+++ /dev/null
@@ -1,311 +0,0 @@
-using Microsoft.EntityFrameworkCore.Migrations;
-
-namespace osu.Game.Migrations
-{
-    public partial class InitialCreate : Migration
-    {
-        protected override void Up(MigrationBuilder migrationBuilder)
-        {
-            migrationBuilder.CreateTable(
-                name: "BeatmapDifficulty",
-                columns: table => new
-                {
-                    ID = table.Column<int>(type: "INTEGER", nullable: false)
-                        .Annotation("Sqlite:Autoincrement", true),
-                    ApproachRate = table.Column<float>(type: "REAL", nullable: false),
-                    CircleSize = table.Column<float>(type: "REAL", nullable: false),
-                    DrainRate = table.Column<float>(type: "REAL", nullable: false),
-                    OverallDifficulty = table.Column<float>(type: "REAL", nullable: false),
-                    SliderMultiplier = table.Column<float>(type: "REAL", nullable: false),
-                    SliderTickRate = table.Column<float>(type: "REAL", nullable: false)
-                },
-                constraints: table =>
-                {
-                    table.PrimaryKey("PK_BeatmapDifficulty", x => x.ID);
-                });
-
-            migrationBuilder.CreateTable(
-                name: "BeatmapMetadata",
-                columns: table => new
-                {
-                    ID = table.Column<int>(type: "INTEGER", nullable: false)
-                        .Annotation("Sqlite:Autoincrement", true),
-                    Artist = table.Column<string>(type: "TEXT", nullable: true),
-                    ArtistUnicode = table.Column<string>(type: "TEXT", nullable: true),
-                    AudioFile = table.Column<string>(type: "TEXT", nullable: true),
-                    Author = table.Column<string>(type: "TEXT", nullable: true),
-                    BackgroundFile = table.Column<string>(type: "TEXT", nullable: true),
-                    PreviewTime = table.Column<int>(type: "INTEGER", nullable: false),
-                    Source = table.Column<string>(type: "TEXT", nullable: true),
-                    Tags = table.Column<string>(type: "TEXT", nullable: true),
-                    Title = table.Column<string>(type: "TEXT", nullable: true),
-                    TitleUnicode = table.Column<string>(type: "TEXT", nullable: true)
-                },
-                constraints: table =>
-                {
-                    table.PrimaryKey("PK_BeatmapMetadata", x => x.ID);
-                });
-
-            migrationBuilder.CreateTable(
-                name: "FileInfo",
-                columns: table => new
-                {
-                    ID = table.Column<int>(type: "INTEGER", nullable: false)
-                        .Annotation("Sqlite:Autoincrement", true),
-                    Hash = table.Column<string>(type: "TEXT", nullable: true),
-                    ReferenceCount = table.Column<int>(type: "INTEGER", nullable: false)
-                },
-                constraints: table =>
-                {
-                    table.PrimaryKey("PK_FileInfo", x => x.ID);
-                });
-
-            migrationBuilder.CreateTable(
-                name: "KeyBinding",
-                columns: table => new
-                {
-                    ID = table.Column<int>(type: "INTEGER", nullable: false)
-                        .Annotation("Sqlite:Autoincrement", true),
-                    Action = table.Column<int>(type: "INTEGER", nullable: false),
-                    Keys = table.Column<string>(type: "TEXT", nullable: true),
-                    RulesetID = table.Column<int>(type: "INTEGER", nullable: true),
-                    Variant = table.Column<int>(type: "INTEGER", nullable: true)
-                },
-                constraints: table =>
-                {
-                    table.PrimaryKey("PK_KeyBinding", x => x.ID);
-                });
-
-            migrationBuilder.CreateTable(
-                name: "RulesetInfo",
-                columns: table => new
-                {
-                    ID = table.Column<int>(type: "INTEGER", nullable: false)
-                        .Annotation("Sqlite:Autoincrement", true),
-                    Available = table.Column<bool>(type: "INTEGER", nullable: false),
-                    InstantiationInfo = table.Column<string>(type: "TEXT", nullable: true),
-                    Name = table.Column<string>(type: "TEXT", nullable: true)
-                },
-                constraints: table =>
-                {
-                    table.PrimaryKey("PK_RulesetInfo", x => x.ID);
-                });
-
-            migrationBuilder.CreateTable(
-                name: "BeatmapSetInfo",
-                columns: table => new
-                {
-                    ID = table.Column<int>(type: "INTEGER", nullable: false)
-                        .Annotation("Sqlite:Autoincrement", true),
-                    DeletePending = table.Column<bool>(type: "INTEGER", nullable: false),
-                    Hash = table.Column<string>(type: "TEXT", nullable: true),
-                    MetadataID = table.Column<int>(type: "INTEGER", nullable: true),
-                    OnlineBeatmapSetID = table.Column<int>(type: "INTEGER", nullable: true),
-                    Protected = table.Column<bool>(type: "INTEGER", nullable: false)
-                },
-                constraints: table =>
-                {
-                    table.PrimaryKey("PK_BeatmapSetInfo", x => x.ID);
-                    table.ForeignKey(
-                        name: "FK_BeatmapSetInfo_BeatmapMetadata_MetadataID",
-                        column: x => x.MetadataID,
-                        principalTable: "BeatmapMetadata",
-                        principalColumn: "ID",
-                        onDelete: ReferentialAction.Restrict);
-                });
-
-            migrationBuilder.CreateTable(
-                name: "BeatmapInfo",
-                columns: table => new
-                {
-                    ID = table.Column<int>(type: "INTEGER", nullable: false)
-                        .Annotation("Sqlite:Autoincrement", true),
-                    AudioLeadIn = table.Column<int>(type: "INTEGER", nullable: false),
-                    BaseDifficultyID = table.Column<int>(type: "INTEGER", nullable: false),
-                    BeatDivisor = table.Column<int>(type: "INTEGER", nullable: false),
-                    BeatmapSetInfoID = table.Column<int>(type: "INTEGER", nullable: false),
-                    Countdown = table.Column<bool>(type: "INTEGER", nullable: false),
-                    DistanceSpacing = table.Column<double>(type: "REAL", nullable: false),
-                    GridSize = table.Column<int>(type: "INTEGER", nullable: false),
-                    Hash = table.Column<string>(type: "TEXT", nullable: true),
-                    Hidden = table.Column<bool>(type: "INTEGER", nullable: false),
-                    LetterboxInBreaks = table.Column<bool>(type: "INTEGER", nullable: false),
-                    MD5Hash = table.Column<string>(type: "TEXT", nullable: true),
-                    MetadataID = table.Column<int>(type: "INTEGER", nullable: true),
-                    OnlineBeatmapID = table.Column<int>(type: "INTEGER", nullable: true),
-                    Path = table.Column<string>(type: "TEXT", nullable: true),
-                    RulesetID = table.Column<int>(type: "INTEGER", nullable: false),
-                    SpecialStyle = table.Column<bool>(type: "INTEGER", nullable: false),
-                    StackLeniency = table.Column<float>(type: "REAL", nullable: false),
-                    StarDifficulty = table.Column<double>(type: "REAL", nullable: false),
-                    StoredBookmarks = table.Column<string>(type: "TEXT", nullable: true),
-                    TimelineZoom = table.Column<double>(type: "REAL", nullable: false),
-                    Version = table.Column<string>(type: "TEXT", nullable: true),
-                    WidescreenStoryboard = table.Column<bool>(type: "INTEGER", nullable: false)
-                },
-                constraints: table =>
-                {
-                    table.PrimaryKey("PK_BeatmapInfo", x => x.ID);
-                    table.ForeignKey(
-                        name: "FK_BeatmapInfo_BeatmapDifficulty_BaseDifficultyID",
-                        column: x => x.BaseDifficultyID,
-                        principalTable: "BeatmapDifficulty",
-                        principalColumn: "ID",
-                        onDelete: ReferentialAction.Cascade);
-                    table.ForeignKey(
-                        name: "FK_BeatmapInfo_BeatmapSetInfo_BeatmapSetInfoID",
-                        column: x => x.BeatmapSetInfoID,
-                        principalTable: "BeatmapSetInfo",
-                        principalColumn: "ID",
-                        onDelete: ReferentialAction.Cascade);
-                    table.ForeignKey(
-                        name: "FK_BeatmapInfo_BeatmapMetadata_MetadataID",
-                        column: x => x.MetadataID,
-                        principalTable: "BeatmapMetadata",
-                        principalColumn: "ID",
-                        onDelete: ReferentialAction.Restrict);
-                    table.ForeignKey(
-                        name: "FK_BeatmapInfo_RulesetInfo_RulesetID",
-                        column: x => x.RulesetID,
-                        principalTable: "RulesetInfo",
-                        principalColumn: "ID",
-                        onDelete: ReferentialAction.Cascade);
-                });
-
-            migrationBuilder.CreateTable(
-                name: "BeatmapSetFileInfo",
-                columns: table => new
-                {
-                    ID = table.Column<int>(type: "INTEGER", nullable: false)
-                        .Annotation("Sqlite:Autoincrement", true),
-                    BeatmapSetInfoID = table.Column<int>(type: "INTEGER", nullable: false),
-                    FileInfoID = table.Column<int>(type: "INTEGER", nullable: false),
-                    Filename = table.Column<string>(type: "TEXT", nullable: false)
-                },
-                constraints: table =>
-                {
-                    table.PrimaryKey("PK_BeatmapSetFileInfo", x => x.ID);
-                    table.ForeignKey(
-                        name: "FK_BeatmapSetFileInfo_BeatmapSetInfo_BeatmapSetInfoID",
-                        column: x => x.BeatmapSetInfoID,
-                        principalTable: "BeatmapSetInfo",
-                        principalColumn: "ID",
-                        onDelete: ReferentialAction.Cascade);
-                    table.ForeignKey(
-                        name: "FK_BeatmapSetFileInfo_FileInfo_FileInfoID",
-                        column: x => x.FileInfoID,
-                        principalTable: "FileInfo",
-                        principalColumn: "ID",
-                        onDelete: ReferentialAction.Cascade);
-                });
-
-            migrationBuilder.CreateIndex(
-                name: "IX_BeatmapInfo_BaseDifficultyID",
-                table: "BeatmapInfo",
-                column: "BaseDifficultyID");
-
-            migrationBuilder.CreateIndex(
-                name: "IX_BeatmapInfo_BeatmapSetInfoID",
-                table: "BeatmapInfo",
-                column: "BeatmapSetInfoID");
-
-            migrationBuilder.CreateIndex(
-                name: "IX_BeatmapInfo_Hash",
-                table: "BeatmapInfo",
-                column: "Hash");
-
-            migrationBuilder.CreateIndex(
-                name: "IX_BeatmapInfo_MD5Hash",
-                table: "BeatmapInfo",
-                column: "MD5Hash");
-
-            migrationBuilder.CreateIndex(
-                name: "IX_BeatmapInfo_MetadataID",
-                table: "BeatmapInfo",
-                column: "MetadataID");
-
-            migrationBuilder.CreateIndex(
-                name: "IX_BeatmapInfo_RulesetID",
-                table: "BeatmapInfo",
-                column: "RulesetID");
-
-            migrationBuilder.CreateIndex(
-                name: "IX_BeatmapSetFileInfo_BeatmapSetInfoID",
-                table: "BeatmapSetFileInfo",
-                column: "BeatmapSetInfoID");
-
-            migrationBuilder.CreateIndex(
-                name: "IX_BeatmapSetFileInfo_FileInfoID",
-                table: "BeatmapSetFileInfo",
-                column: "FileInfoID");
-
-            migrationBuilder.CreateIndex(
-                name: "IX_BeatmapSetInfo_DeletePending",
-                table: "BeatmapSetInfo",
-                column: "DeletePending");
-
-            migrationBuilder.CreateIndex(
-                name: "IX_BeatmapSetInfo_Hash",
-                table: "BeatmapSetInfo",
-                column: "Hash");
-
-            migrationBuilder.CreateIndex(
-                name: "IX_BeatmapSetInfo_MetadataID",
-                table: "BeatmapSetInfo",
-                column: "MetadataID");
-
-            migrationBuilder.CreateIndex(
-                name: "IX_FileInfo_Hash",
-                table: "FileInfo",
-                column: "Hash",
-                unique: true);
-
-            migrationBuilder.CreateIndex(
-                name: "IX_FileInfo_ReferenceCount",
-                table: "FileInfo",
-                column: "ReferenceCount");
-
-            migrationBuilder.CreateIndex(
-                name: "IX_KeyBinding_Action",
-                table: "KeyBinding",
-                column: "Action");
-
-            migrationBuilder.CreateIndex(
-                name: "IX_KeyBinding_Variant",
-                table: "KeyBinding",
-                column: "Variant");
-
-            migrationBuilder.CreateIndex(
-                name: "IX_RulesetInfo_Available",
-                table: "RulesetInfo",
-                column: "Available");
-        }
-
-        protected override void Down(MigrationBuilder migrationBuilder)
-        {
-            migrationBuilder.DropTable(
-                name: "BeatmapInfo");
-
-            migrationBuilder.DropTable(
-                name: "BeatmapSetFileInfo");
-
-            migrationBuilder.DropTable(
-                name: "KeyBinding");
-
-            migrationBuilder.DropTable(
-                name: "BeatmapDifficulty");
-
-            migrationBuilder.DropTable(
-                name: "RulesetInfo");
-
-            migrationBuilder.DropTable(
-                name: "BeatmapSetInfo");
-
-            migrationBuilder.DropTable(
-                name: "FileInfo");
-
-            migrationBuilder.DropTable(
-                name: "BeatmapMetadata");
-        }
-    }
-}
diff --git a/osu.Game/Migrations/20171025071459_AddMissingIndexRules.Designer.cs b/osu.Game/Migrations/20171025071459_AddMissingIndexRules.Designer.cs
deleted file mode 100644
index 4cd234f2ef..0000000000
--- a/osu.Game/Migrations/20171025071459_AddMissingIndexRules.Designer.cs
+++ /dev/null
@@ -1,299 +0,0 @@
-// <auto-generated />
-using Microsoft.EntityFrameworkCore;
-using Microsoft.EntityFrameworkCore.Infrastructure;
-using Microsoft.EntityFrameworkCore.Metadata;
-using Microsoft.EntityFrameworkCore.Migrations;
-using Microsoft.EntityFrameworkCore.Storage;
-using osu.Game.Database;
-using System;
-
-namespace osu.Game.Migrations
-{
-    [DbContext(typeof(OsuDbContext))]
-    [Migration("20171025071459_AddMissingIndexRules")]
-    partial class AddMissingIndexRules
-    {
-        protected override void BuildTargetModel(ModelBuilder modelBuilder)
-        {
-#pragma warning disable 612, 618
-            modelBuilder
-                .HasAnnotation("ProductVersion", "2.0.0-rtm-26452");
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapDifficulty", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<float>("ApproachRate");
-
-                    b.Property<float>("CircleSize");
-
-                    b.Property<float>("DrainRate");
-
-                    b.Property<float>("OverallDifficulty");
-
-                    b.Property<float>("SliderMultiplier");
-
-                    b.Property<float>("SliderTickRate");
-
-                    b.HasKey("ID");
-
-                    b.ToTable("BeatmapDifficulty");
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<int>("AudioLeadIn");
-
-                    b.Property<int>("BaseDifficultyID");
-
-                    b.Property<int>("BeatDivisor");
-
-                    b.Property<int>("BeatmapSetInfoID");
-
-                    b.Property<bool>("Countdown");
-
-                    b.Property<double>("DistanceSpacing");
-
-                    b.Property<int>("GridSize");
-
-                    b.Property<string>("Hash");
-
-                    b.Property<bool>("Hidden");
-
-                    b.Property<bool>("LetterboxInBreaks");
-
-                    b.Property<string>("MD5Hash");
-
-                    b.Property<int?>("MetadataID");
-
-                    b.Property<int?>("OnlineBeatmapID");
-
-                    b.Property<string>("Path");
-
-                    b.Property<int>("RulesetID");
-
-                    b.Property<bool>("SpecialStyle");
-
-                    b.Property<float>("StackLeniency");
-
-                    b.Property<double>("StarDifficulty");
-
-                    b.Property<string>("StoredBookmarks");
-
-                    b.Property<double>("TimelineZoom");
-
-                    b.Property<string>("Version");
-
-                    b.Property<bool>("WidescreenStoryboard");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("BaseDifficultyID");
-
-                    b.HasIndex("BeatmapSetInfoID");
-
-                    b.HasIndex("Hash")
-                        .IsUnique();
-
-                    b.HasIndex("MD5Hash")
-                        .IsUnique();
-
-                    b.HasIndex("MetadataID");
-
-                    b.HasIndex("RulesetID");
-
-                    b.ToTable("BeatmapInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapMetadata", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<string>("Artist");
-
-                    b.Property<string>("ArtistUnicode");
-
-                    b.Property<string>("AudioFile");
-
-                    b.Property<string>("AuthorString")
-                        .HasColumnName("Author");
-
-                    b.Property<string>("BackgroundFile");
-
-                    b.Property<int>("PreviewTime");
-
-                    b.Property<string>("Source");
-
-                    b.Property<string>("Tags");
-
-                    b.Property<string>("Title");
-
-                    b.Property<string>("TitleUnicode");
-
-                    b.HasKey("ID");
-
-                    b.ToTable("BeatmapMetadata");
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<int>("BeatmapSetInfoID");
-
-                    b.Property<int>("FileInfoID");
-
-                    b.Property<string>("Filename")
-                        .IsRequired();
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("BeatmapSetInfoID");
-
-                    b.HasIndex("FileInfoID");
-
-                    b.ToTable("BeatmapSetFileInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<bool>("DeletePending");
-
-                    b.Property<string>("Hash");
-
-                    b.Property<int?>("MetadataID");
-
-                    b.Property<int?>("OnlineBeatmapSetID");
-
-                    b.Property<bool>("Protected");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("DeletePending");
-
-                    b.HasIndex("Hash")
-                        .IsUnique();
-
-                    b.HasIndex("MetadataID");
-
-                    b.HasIndex("OnlineBeatmapSetID")
-                        .IsUnique();
-
-                    b.ToTable("BeatmapSetInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Input.Bindings.DatabasedKeyBinding", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<int>("IntAction")
-                        .HasColumnName("Action");
-
-                    b.Property<string>("KeysString")
-                        .HasColumnName("Keys");
-
-                    b.Property<int?>("RulesetID");
-
-                    b.Property<int?>("Variant");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("IntAction");
-
-                    b.HasIndex("Variant");
-
-                    b.ToTable("KeyBinding");
-                });
-
-            modelBuilder.Entity("osu.Game.IO.FileInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<string>("Hash");
-
-                    b.Property<int>("ReferenceCount");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("Hash")
-                        .IsUnique();
-
-                    b.HasIndex("ReferenceCount");
-
-                    b.ToTable("FileInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Rulesets.RulesetInfo", b =>
-                {
-                    b.Property<int?>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<bool>("Available");
-
-                    b.Property<string>("InstantiationInfo");
-
-                    b.Property<string>("Name");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("Available");
-
-                    b.ToTable("RulesetInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b =>
-                {
-                    b.HasOne("osu.Game.Beatmaps.BeatmapDifficulty", "BaseDifficulty")
-                        .WithMany()
-                        .HasForeignKey("BaseDifficultyID")
-                        .OnDelete(DeleteBehavior.Cascade);
-
-                    b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo", "BeatmapSet")
-                        .WithMany("Beatmaps")
-                        .HasForeignKey("BeatmapSetInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-
-                    b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata")
-                        .WithMany("Beatmaps")
-                        .HasForeignKey("MetadataID");
-
-                    b.HasOne("osu.Game.Rulesets.RulesetInfo", "Ruleset")
-                        .WithMany()
-                        .HasForeignKey("RulesetID")
-                        .OnDelete(DeleteBehavior.Cascade);
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b =>
-                {
-                    b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo")
-                        .WithMany("Files")
-                        .HasForeignKey("BeatmapSetInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-
-                    b.HasOne("osu.Game.IO.FileInfo", "FileInfo")
-                        .WithMany()
-                        .HasForeignKey("FileInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b =>
-                {
-                    b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata")
-                        .WithMany("BeatmapSets")
-                        .HasForeignKey("MetadataID");
-                });
-#pragma warning restore 612, 618
-        }
-    }
-}
diff --git a/osu.Game/Migrations/20171025071459_AddMissingIndexRules.cs b/osu.Game/Migrations/20171025071459_AddMissingIndexRules.cs
deleted file mode 100644
index c9fc59c5a2..0000000000
--- a/osu.Game/Migrations/20171025071459_AddMissingIndexRules.cs
+++ /dev/null
@@ -1,80 +0,0 @@
-using Microsoft.EntityFrameworkCore.Migrations;
-
-namespace osu.Game.Migrations
-{
-    public partial class AddMissingIndexRules : Migration
-    {
-        protected override void Up(MigrationBuilder migrationBuilder)
-        {
-            migrationBuilder.DropIndex(
-                name: "IX_BeatmapSetInfo_Hash",
-                table: "BeatmapSetInfo");
-
-            migrationBuilder.DropIndex(
-                name: "IX_BeatmapInfo_Hash",
-                table: "BeatmapInfo");
-
-            migrationBuilder.DropIndex(
-                name: "IX_BeatmapInfo_MD5Hash",
-                table: "BeatmapInfo");
-
-            migrationBuilder.CreateIndex(
-                name: "IX_BeatmapSetInfo_Hash",
-                table: "BeatmapSetInfo",
-                column: "Hash",
-                unique: true);
-
-            migrationBuilder.CreateIndex(
-                name: "IX_BeatmapSetInfo_OnlineBeatmapSetID",
-                table: "BeatmapSetInfo",
-                column: "OnlineBeatmapSetID",
-                unique: true);
-
-            migrationBuilder.CreateIndex(
-                name: "IX_BeatmapInfo_Hash",
-                table: "BeatmapInfo",
-                column: "Hash",
-                unique: true);
-
-            migrationBuilder.CreateIndex(
-                name: "IX_BeatmapInfo_MD5Hash",
-                table: "BeatmapInfo",
-                column: "MD5Hash",
-                unique: true);
-        }
-
-        protected override void Down(MigrationBuilder migrationBuilder)
-        {
-            migrationBuilder.DropIndex(
-                name: "IX_BeatmapSetInfo_Hash",
-                table: "BeatmapSetInfo");
-
-            migrationBuilder.DropIndex(
-                name: "IX_BeatmapSetInfo_OnlineBeatmapSetID",
-                table: "BeatmapSetInfo");
-
-            migrationBuilder.DropIndex(
-                name: "IX_BeatmapInfo_Hash",
-                table: "BeatmapInfo");
-
-            migrationBuilder.DropIndex(
-                name: "IX_BeatmapInfo_MD5Hash",
-                table: "BeatmapInfo");
-
-            migrationBuilder.CreateIndex(
-                name: "IX_BeatmapSetInfo_Hash",
-                table: "BeatmapSetInfo",
-                column: "Hash");
-
-            migrationBuilder.CreateIndex(
-                name: "IX_BeatmapInfo_Hash",
-                table: "BeatmapInfo",
-                column: "Hash");
-
-            migrationBuilder.CreateIndex(
-                name: "IX_BeatmapInfo_MD5Hash",
-                table: "BeatmapInfo",
-                column: "MD5Hash");
-        }
-    }
-}
diff --git a/osu.Game/Migrations/20171119065731_AddBeatmapOnlineIDUniqueConstraint.Designer.cs b/osu.Game/Migrations/20171119065731_AddBeatmapOnlineIDUniqueConstraint.Designer.cs
deleted file mode 100644
index 006acf12cd..0000000000
--- a/osu.Game/Migrations/20171119065731_AddBeatmapOnlineIDUniqueConstraint.Designer.cs
+++ /dev/null
@@ -1,302 +0,0 @@
-// <auto-generated />
-using Microsoft.EntityFrameworkCore;
-using Microsoft.EntityFrameworkCore.Infrastructure;
-using Microsoft.EntityFrameworkCore.Metadata;
-using Microsoft.EntityFrameworkCore.Migrations;
-using Microsoft.EntityFrameworkCore.Storage;
-using osu.Game.Database;
-using System;
-
-namespace osu.Game.Migrations
-{
-    [DbContext(typeof(OsuDbContext))]
-    [Migration("20171119065731_AddBeatmapOnlineIDUniqueConstraint")]
-    partial class AddBeatmapOnlineIDUniqueConstraint
-    {
-        protected override void BuildTargetModel(ModelBuilder modelBuilder)
-        {
-#pragma warning disable 612, 618
-            modelBuilder
-                .HasAnnotation("ProductVersion", "2.0.0-rtm-26452");
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapDifficulty", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<float>("ApproachRate");
-
-                    b.Property<float>("CircleSize");
-
-                    b.Property<float>("DrainRate");
-
-                    b.Property<float>("OverallDifficulty");
-
-                    b.Property<float>("SliderMultiplier");
-
-                    b.Property<float>("SliderTickRate");
-
-                    b.HasKey("ID");
-
-                    b.ToTable("BeatmapDifficulty");
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<int>("AudioLeadIn");
-
-                    b.Property<int>("BaseDifficultyID");
-
-                    b.Property<int>("BeatDivisor");
-
-                    b.Property<int>("BeatmapSetInfoID");
-
-                    b.Property<bool>("Countdown");
-
-                    b.Property<double>("DistanceSpacing");
-
-                    b.Property<int>("GridSize");
-
-                    b.Property<string>("Hash");
-
-                    b.Property<bool>("Hidden");
-
-                    b.Property<bool>("LetterboxInBreaks");
-
-                    b.Property<string>("MD5Hash");
-
-                    b.Property<int?>("MetadataID");
-
-                    b.Property<int?>("OnlineBeatmapID");
-
-                    b.Property<string>("Path");
-
-                    b.Property<int>("RulesetID");
-
-                    b.Property<bool>("SpecialStyle");
-
-                    b.Property<float>("StackLeniency");
-
-                    b.Property<double>("StarDifficulty");
-
-                    b.Property<string>("StoredBookmarks");
-
-                    b.Property<double>("TimelineZoom");
-
-                    b.Property<string>("Version");
-
-                    b.Property<bool>("WidescreenStoryboard");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("BaseDifficultyID");
-
-                    b.HasIndex("BeatmapSetInfoID");
-
-                    b.HasIndex("Hash")
-                        .IsUnique();
-
-                    b.HasIndex("MD5Hash")
-                        .IsUnique();
-
-                    b.HasIndex("MetadataID");
-
-                    b.HasIndex("OnlineBeatmapID")
-                        .IsUnique();
-
-                    b.HasIndex("RulesetID");
-
-                    b.ToTable("BeatmapInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapMetadata", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<string>("Artist");
-
-                    b.Property<string>("ArtistUnicode");
-
-                    b.Property<string>("AudioFile");
-
-                    b.Property<string>("AuthorString")
-                        .HasColumnName("Author");
-
-                    b.Property<string>("BackgroundFile");
-
-                    b.Property<int>("PreviewTime");
-
-                    b.Property<string>("Source");
-
-                    b.Property<string>("Tags");
-
-                    b.Property<string>("Title");
-
-                    b.Property<string>("TitleUnicode");
-
-                    b.HasKey("ID");
-
-                    b.ToTable("BeatmapMetadata");
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<int>("BeatmapSetInfoID");
-
-                    b.Property<int>("FileInfoID");
-
-                    b.Property<string>("Filename")
-                        .IsRequired();
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("BeatmapSetInfoID");
-
-                    b.HasIndex("FileInfoID");
-
-                    b.ToTable("BeatmapSetFileInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<bool>("DeletePending");
-
-                    b.Property<string>("Hash");
-
-                    b.Property<int?>("MetadataID");
-
-                    b.Property<int?>("OnlineBeatmapSetID");
-
-                    b.Property<bool>("Protected");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("DeletePending");
-
-                    b.HasIndex("Hash")
-                        .IsUnique();
-
-                    b.HasIndex("MetadataID");
-
-                    b.HasIndex("OnlineBeatmapSetID")
-                        .IsUnique();
-
-                    b.ToTable("BeatmapSetInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Input.Bindings.DatabasedKeyBinding", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<int>("IntAction")
-                        .HasColumnName("Action");
-
-                    b.Property<string>("KeysString")
-                        .HasColumnName("Keys");
-
-                    b.Property<int?>("RulesetID");
-
-                    b.Property<int?>("Variant");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("IntAction");
-
-                    b.HasIndex("Variant");
-
-                    b.ToTable("KeyBinding");
-                });
-
-            modelBuilder.Entity("osu.Game.IO.FileInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<string>("Hash");
-
-                    b.Property<int>("ReferenceCount");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("Hash")
-                        .IsUnique();
-
-                    b.HasIndex("ReferenceCount");
-
-                    b.ToTable("FileInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Rulesets.RulesetInfo", b =>
-                {
-                    b.Property<int?>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<bool>("Available");
-
-                    b.Property<string>("InstantiationInfo");
-
-                    b.Property<string>("Name");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("Available");
-
-                    b.ToTable("RulesetInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b =>
-                {
-                    b.HasOne("osu.Game.Beatmaps.BeatmapDifficulty", "BaseDifficulty")
-                        .WithMany()
-                        .HasForeignKey("BaseDifficultyID")
-                        .OnDelete(DeleteBehavior.Cascade);
-
-                    b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo", "BeatmapSet")
-                        .WithMany("Beatmaps")
-                        .HasForeignKey("BeatmapSetInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-
-                    b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata")
-                        .WithMany("Beatmaps")
-                        .HasForeignKey("MetadataID");
-
-                    b.HasOne("osu.Game.Rulesets.RulesetInfo", "Ruleset")
-                        .WithMany()
-                        .HasForeignKey("RulesetID")
-                        .OnDelete(DeleteBehavior.Cascade);
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b =>
-                {
-                    b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo")
-                        .WithMany("Files")
-                        .HasForeignKey("BeatmapSetInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-
-                    b.HasOne("osu.Game.IO.FileInfo", "FileInfo")
-                        .WithMany()
-                        .HasForeignKey("FileInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b =>
-                {
-                    b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata")
-                        .WithMany("BeatmapSets")
-                        .HasForeignKey("MetadataID");
-                });
-#pragma warning restore 612, 618
-        }
-    }
-}
diff --git a/osu.Game/Migrations/20171119065731_AddBeatmapOnlineIDUniqueConstraint.cs b/osu.Game/Migrations/20171119065731_AddBeatmapOnlineIDUniqueConstraint.cs
deleted file mode 100644
index 084ae67940..0000000000
--- a/osu.Game/Migrations/20171119065731_AddBeatmapOnlineIDUniqueConstraint.cs
+++ /dev/null
@@ -1,23 +0,0 @@
-using Microsoft.EntityFrameworkCore.Migrations;
-
-namespace osu.Game.Migrations
-{
-    public partial class AddBeatmapOnlineIDUniqueConstraint : Migration
-    {
-        protected override void Up(MigrationBuilder migrationBuilder)
-        {
-            migrationBuilder.CreateIndex(
-                name: "IX_BeatmapInfo_OnlineBeatmapID",
-                table: "BeatmapInfo",
-                column: "OnlineBeatmapID",
-                unique: true);
-        }
-
-        protected override void Down(MigrationBuilder migrationBuilder)
-        {
-            migrationBuilder.DropIndex(
-                name: "IX_BeatmapInfo_OnlineBeatmapID",
-                table: "BeatmapInfo");
-        }
-    }
-}
diff --git a/osu.Game/Migrations/20171209034410_AddRulesetInfoShortName.Designer.cs b/osu.Game/Migrations/20171209034410_AddRulesetInfoShortName.Designer.cs
deleted file mode 100644
index fc2496bc24..0000000000
--- a/osu.Game/Migrations/20171209034410_AddRulesetInfoShortName.Designer.cs
+++ /dev/null
@@ -1,307 +0,0 @@
-// <auto-generated />
-using Microsoft.EntityFrameworkCore;
-using Microsoft.EntityFrameworkCore.Infrastructure;
-using Microsoft.EntityFrameworkCore.Metadata;
-using Microsoft.EntityFrameworkCore.Migrations;
-using Microsoft.EntityFrameworkCore.Storage;
-using osu.Game.Database;
-using System;
-
-namespace osu.Game.Migrations
-{
-    [DbContext(typeof(OsuDbContext))]
-    [Migration("20171209034410_AddRulesetInfoShortName")]
-    partial class AddRulesetInfoShortName
-    {
-        protected override void BuildTargetModel(ModelBuilder modelBuilder)
-        {
-#pragma warning disable 612, 618
-            modelBuilder
-                .HasAnnotation("ProductVersion", "2.0.0-rtm-26452");
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapDifficulty", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<float>("ApproachRate");
-
-                    b.Property<float>("CircleSize");
-
-                    b.Property<float>("DrainRate");
-
-                    b.Property<float>("OverallDifficulty");
-
-                    b.Property<float>("SliderMultiplier");
-
-                    b.Property<float>("SliderTickRate");
-
-                    b.HasKey("ID");
-
-                    b.ToTable("BeatmapDifficulty");
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<int>("AudioLeadIn");
-
-                    b.Property<int>("BaseDifficultyID");
-
-                    b.Property<int>("BeatDivisor");
-
-                    b.Property<int>("BeatmapSetInfoID");
-
-                    b.Property<bool>("Countdown");
-
-                    b.Property<double>("DistanceSpacing");
-
-                    b.Property<int>("GridSize");
-
-                    b.Property<string>("Hash");
-
-                    b.Property<bool>("Hidden");
-
-                    b.Property<bool>("LetterboxInBreaks");
-
-                    b.Property<string>("MD5Hash");
-
-                    b.Property<int?>("MetadataID");
-
-                    b.Property<int?>("OnlineBeatmapID");
-
-                    b.Property<string>("Path");
-
-                    b.Property<int>("RulesetID");
-
-                    b.Property<bool>("SpecialStyle");
-
-                    b.Property<float>("StackLeniency");
-
-                    b.Property<double>("StarDifficulty");
-
-                    b.Property<string>("StoredBookmarks");
-
-                    b.Property<double>("TimelineZoom");
-
-                    b.Property<string>("Version");
-
-                    b.Property<bool>("WidescreenStoryboard");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("BaseDifficultyID");
-
-                    b.HasIndex("BeatmapSetInfoID");
-
-                    b.HasIndex("Hash")
-                        .IsUnique();
-
-                    b.HasIndex("MD5Hash")
-                        .IsUnique();
-
-                    b.HasIndex("MetadataID");
-
-                    b.HasIndex("OnlineBeatmapID")
-                        .IsUnique();
-
-                    b.HasIndex("RulesetID");
-
-                    b.ToTable("BeatmapInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapMetadata", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<string>("Artist");
-
-                    b.Property<string>("ArtistUnicode");
-
-                    b.Property<string>("AudioFile");
-
-                    b.Property<string>("AuthorString")
-                        .HasColumnName("Author");
-
-                    b.Property<string>("BackgroundFile");
-
-                    b.Property<int>("PreviewTime");
-
-                    b.Property<string>("Source");
-
-                    b.Property<string>("Tags");
-
-                    b.Property<string>("Title");
-
-                    b.Property<string>("TitleUnicode");
-
-                    b.HasKey("ID");
-
-                    b.ToTable("BeatmapMetadata");
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<int>("BeatmapSetInfoID");
-
-                    b.Property<int>("FileInfoID");
-
-                    b.Property<string>("Filename")
-                        .IsRequired();
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("BeatmapSetInfoID");
-
-                    b.HasIndex("FileInfoID");
-
-                    b.ToTable("BeatmapSetFileInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<bool>("DeletePending");
-
-                    b.Property<string>("Hash");
-
-                    b.Property<int?>("MetadataID");
-
-                    b.Property<int?>("OnlineBeatmapSetID");
-
-                    b.Property<bool>("Protected");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("DeletePending");
-
-                    b.HasIndex("Hash")
-                        .IsUnique();
-
-                    b.HasIndex("MetadataID");
-
-                    b.HasIndex("OnlineBeatmapSetID")
-                        .IsUnique();
-
-                    b.ToTable("BeatmapSetInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Input.Bindings.DatabasedKeyBinding", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<int>("IntAction")
-                        .HasColumnName("Action");
-
-                    b.Property<string>("KeysString")
-                        .HasColumnName("Keys");
-
-                    b.Property<int?>("RulesetID");
-
-                    b.Property<int?>("Variant");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("IntAction");
-
-                    b.HasIndex("Variant");
-
-                    b.ToTable("KeyBinding");
-                });
-
-            modelBuilder.Entity("osu.Game.IO.FileInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<string>("Hash");
-
-                    b.Property<int>("ReferenceCount");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("Hash")
-                        .IsUnique();
-
-                    b.HasIndex("ReferenceCount");
-
-                    b.ToTable("FileInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Rulesets.RulesetInfo", b =>
-                {
-                    b.Property<int?>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<bool>("Available");
-
-                    b.Property<string>("InstantiationInfo");
-
-                    b.Property<string>("Name");
-
-                    b.Property<string>("ShortName");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("Available");
-
-                    b.HasIndex("ShortName")
-                        .IsUnique();
-
-                    b.ToTable("RulesetInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b =>
-                {
-                    b.HasOne("osu.Game.Beatmaps.BeatmapDifficulty", "BaseDifficulty")
-                        .WithMany()
-                        .HasForeignKey("BaseDifficultyID")
-                        .OnDelete(DeleteBehavior.Cascade);
-
-                    b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo", "BeatmapSet")
-                        .WithMany("Beatmaps")
-                        .HasForeignKey("BeatmapSetInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-
-                    b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata")
-                        .WithMany("Beatmaps")
-                        .HasForeignKey("MetadataID");
-
-                    b.HasOne("osu.Game.Rulesets.RulesetInfo", "Ruleset")
-                        .WithMany()
-                        .HasForeignKey("RulesetID")
-                        .OnDelete(DeleteBehavior.Cascade);
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b =>
-                {
-                    b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo")
-                        .WithMany("Files")
-                        .HasForeignKey("BeatmapSetInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-
-                    b.HasOne("osu.Game.IO.FileInfo", "FileInfo")
-                        .WithMany()
-                        .HasForeignKey("FileInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b =>
-                {
-                    b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata")
-                        .WithMany("BeatmapSets")
-                        .HasForeignKey("MetadataID");
-                });
-#pragma warning restore 612, 618
-        }
-    }
-}
diff --git a/osu.Game/Migrations/20171209034410_AddRulesetInfoShortName.cs b/osu.Game/Migrations/20171209034410_AddRulesetInfoShortName.cs
deleted file mode 100644
index 09cf0af89c..0000000000
--- a/osu.Game/Migrations/20171209034410_AddRulesetInfoShortName.cs
+++ /dev/null
@@ -1,33 +0,0 @@
-using Microsoft.EntityFrameworkCore.Migrations;
-
-namespace osu.Game.Migrations
-{
-    public partial class AddRulesetInfoShortName : Migration
-    {
-        protected override void Up(MigrationBuilder migrationBuilder)
-        {
-            migrationBuilder.AddColumn<string>(
-                name: "ShortName",
-                table: "RulesetInfo",
-                type: "TEXT",
-                nullable: true);
-
-            migrationBuilder.CreateIndex(
-                name: "IX_RulesetInfo_ShortName",
-                table: "RulesetInfo",
-                column: "ShortName",
-                unique: true);
-        }
-
-        protected override void Down(MigrationBuilder migrationBuilder)
-        {
-            migrationBuilder.DropIndex(
-                name: "IX_RulesetInfo_ShortName",
-                table: "RulesetInfo");
-
-            migrationBuilder.DropColumn(
-                name: "ShortName",
-                table: "RulesetInfo");
-        }
-    }
-}
diff --git a/osu.Game/Migrations/20180125143340_Settings.Designer.cs b/osu.Game/Migrations/20180125143340_Settings.Designer.cs
deleted file mode 100644
index 4bb599eec1..0000000000
--- a/osu.Game/Migrations/20180125143340_Settings.Designer.cs
+++ /dev/null
@@ -1,329 +0,0 @@
-// <auto-generated />
-using Microsoft.EntityFrameworkCore;
-using Microsoft.EntityFrameworkCore.Infrastructure;
-using Microsoft.EntityFrameworkCore.Metadata;
-using Microsoft.EntityFrameworkCore.Migrations;
-using Microsoft.EntityFrameworkCore.Storage;
-using osu.Game.Database;
-using System;
-
-namespace osu.Game.Migrations
-{
-    [DbContext(typeof(OsuDbContext))]
-    [Migration("20180125143340_Settings")]
-    partial class Settings
-    {
-        protected override void BuildTargetModel(ModelBuilder modelBuilder)
-        {
-#pragma warning disable 612, 618
-            modelBuilder
-                .HasAnnotation("ProductVersion", "2.0.0-rtm-26452");
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapDifficulty", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<float>("ApproachRate");
-
-                    b.Property<float>("CircleSize");
-
-                    b.Property<float>("DrainRate");
-
-                    b.Property<float>("OverallDifficulty");
-
-                    b.Property<float>("SliderMultiplier");
-
-                    b.Property<float>("SliderTickRate");
-
-                    b.HasKey("ID");
-
-                    b.ToTable("BeatmapDifficulty");
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<int>("AudioLeadIn");
-
-                    b.Property<int>("BaseDifficultyID");
-
-                    b.Property<int>("BeatDivisor");
-
-                    b.Property<int>("BeatmapSetInfoID");
-
-                    b.Property<bool>("Countdown");
-
-                    b.Property<double>("DistanceSpacing");
-
-                    b.Property<int>("GridSize");
-
-                    b.Property<string>("Hash");
-
-                    b.Property<bool>("Hidden");
-
-                    b.Property<bool>("LetterboxInBreaks");
-
-                    b.Property<string>("MD5Hash");
-
-                    b.Property<int?>("MetadataID");
-
-                    b.Property<int?>("OnlineBeatmapID");
-
-                    b.Property<string>("Path");
-
-                    b.Property<int>("RulesetID");
-
-                    b.Property<bool>("SpecialStyle");
-
-                    b.Property<float>("StackLeniency");
-
-                    b.Property<double>("StarDifficulty");
-
-                    b.Property<string>("StoredBookmarks");
-
-                    b.Property<double>("TimelineZoom");
-
-                    b.Property<string>("Version");
-
-                    b.Property<bool>("WidescreenStoryboard");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("BaseDifficultyID");
-
-                    b.HasIndex("BeatmapSetInfoID");
-
-                    b.HasIndex("Hash")
-                        .IsUnique();
-
-                    b.HasIndex("MD5Hash")
-                        .IsUnique();
-
-                    b.HasIndex("MetadataID");
-
-                    b.HasIndex("OnlineBeatmapID")
-                        .IsUnique();
-
-                    b.HasIndex("RulesetID");
-
-                    b.ToTable("BeatmapInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapMetadata", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<string>("Artist");
-
-                    b.Property<string>("ArtistUnicode");
-
-                    b.Property<string>("AudioFile");
-
-                    b.Property<string>("AuthorString")
-                        .HasColumnName("Author");
-
-                    b.Property<string>("BackgroundFile");
-
-                    b.Property<int>("PreviewTime");
-
-                    b.Property<string>("Source");
-
-                    b.Property<string>("Tags");
-
-                    b.Property<string>("Title");
-
-                    b.Property<string>("TitleUnicode");
-
-                    b.HasKey("ID");
-
-                    b.ToTable("BeatmapMetadata");
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<int>("BeatmapSetInfoID");
-
-                    b.Property<int>("FileInfoID");
-
-                    b.Property<string>("Filename")
-                        .IsRequired();
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("BeatmapSetInfoID");
-
-                    b.HasIndex("FileInfoID");
-
-                    b.ToTable("BeatmapSetFileInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<bool>("DeletePending");
-
-                    b.Property<string>("Hash");
-
-                    b.Property<int?>("MetadataID");
-
-                    b.Property<int?>("OnlineBeatmapSetID");
-
-                    b.Property<bool>("Protected");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("DeletePending");
-
-                    b.HasIndex("Hash")
-                        .IsUnique();
-
-                    b.HasIndex("MetadataID");
-
-                    b.HasIndex("OnlineBeatmapSetID")
-                        .IsUnique();
-
-                    b.ToTable("BeatmapSetInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Configuration.DatabasedSetting", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<int>("IntKey")
-                        .HasColumnName("Key");
-
-                    b.Property<int?>("RulesetID");
-
-                    b.Property<string>("StringValue")
-                        .HasColumnName("Value");
-
-                    b.Property<int?>("Variant");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("RulesetID", "Variant");
-
-                    b.ToTable("Settings");
-                });
-
-            modelBuilder.Entity("osu.Game.Input.Bindings.DatabasedKeyBinding", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<int>("IntAction")
-                        .HasColumnName("Action");
-
-                    b.Property<string>("KeysString")
-                        .HasColumnName("Keys");
-
-                    b.Property<int?>("RulesetID");
-
-                    b.Property<int?>("Variant");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("IntAction");
-
-                    b.HasIndex("RulesetID", "Variant");
-
-                    b.ToTable("KeyBinding");
-                });
-
-            modelBuilder.Entity("osu.Game.IO.FileInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<string>("Hash");
-
-                    b.Property<int>("ReferenceCount");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("Hash")
-                        .IsUnique();
-
-                    b.HasIndex("ReferenceCount");
-
-                    b.ToTable("FileInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Rulesets.RulesetInfo", b =>
-                {
-                    b.Property<int?>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<bool>("Available");
-
-                    b.Property<string>("InstantiationInfo");
-
-                    b.Property<string>("Name");
-
-                    b.Property<string>("ShortName");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("Available");
-
-                    b.HasIndex("ShortName")
-                        .IsUnique();
-
-                    b.ToTable("RulesetInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b =>
-                {
-                    b.HasOne("osu.Game.Beatmaps.BeatmapDifficulty", "BaseDifficulty")
-                        .WithMany()
-                        .HasForeignKey("BaseDifficultyID")
-                        .OnDelete(DeleteBehavior.Cascade);
-
-                    b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo", "BeatmapSet")
-                        .WithMany("Beatmaps")
-                        .HasForeignKey("BeatmapSetInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-
-                    b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata")
-                        .WithMany("Beatmaps")
-                        .HasForeignKey("MetadataID");
-
-                    b.HasOne("osu.Game.Rulesets.RulesetInfo", "Ruleset")
-                        .WithMany()
-                        .HasForeignKey("RulesetID")
-                        .OnDelete(DeleteBehavior.Cascade);
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b =>
-                {
-                    b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo")
-                        .WithMany("Files")
-                        .HasForeignKey("BeatmapSetInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-
-                    b.HasOne("osu.Game.IO.FileInfo", "FileInfo")
-                        .WithMany()
-                        .HasForeignKey("FileInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b =>
-                {
-                    b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata")
-                        .WithMany("BeatmapSets")
-                        .HasForeignKey("MetadataID");
-                });
-#pragma warning restore 612, 618
-        }
-    }
-}
diff --git a/osu.Game/Migrations/20180125143340_Settings.cs b/osu.Game/Migrations/20180125143340_Settings.cs
deleted file mode 100644
index 166d3c086d..0000000000
--- a/osu.Game/Migrations/20180125143340_Settings.cs
+++ /dev/null
@@ -1,55 +0,0 @@
-using Microsoft.EntityFrameworkCore.Migrations;
-
-namespace osu.Game.Migrations
-{
-    public partial class Settings : Migration
-    {
-        protected override void Up(MigrationBuilder migrationBuilder)
-        {
-            migrationBuilder.DropIndex(
-                name: "IX_KeyBinding_Variant",
-                table: "KeyBinding");
-
-            migrationBuilder.CreateTable(
-                name: "Settings",
-                columns: table => new
-                {
-                    ID = table.Column<int>(type: "INTEGER", nullable: false)
-                        .Annotation("Sqlite:Autoincrement", true),
-                    Key = table.Column<int>(type: "TEXT", nullable: false),
-                    RulesetID = table.Column<int>(type: "INTEGER", nullable: true),
-                    Value = table.Column<string>(type: "TEXT", nullable: true),
-                    Variant = table.Column<int>(type: "INTEGER", nullable: true)
-                },
-                constraints: table =>
-                {
-                    table.PrimaryKey("PK_Settings", x => x.ID);
-                });
-
-            migrationBuilder.CreateIndex(
-                name: "IX_KeyBinding_RulesetID_Variant",
-                table: "KeyBinding",
-                columns: new[] { "RulesetID", "Variant" });
-
-            migrationBuilder.CreateIndex(
-                name: "IX_Settings_RulesetID_Variant",
-                table: "Settings",
-                columns: new[] { "RulesetID", "Variant" });
-        }
-
-        protected override void Down(MigrationBuilder migrationBuilder)
-        {
-            migrationBuilder.DropTable(
-                name: "Settings");
-
-            migrationBuilder.DropIndex(
-                name: "IX_KeyBinding_RulesetID_Variant",
-                table: "KeyBinding");
-
-            migrationBuilder.CreateIndex(
-                name: "IX_KeyBinding_Variant",
-                table: "KeyBinding",
-                column: "Variant");
-        }
-    }
-}
diff --git a/osu.Game/Migrations/20180131154205_AddMuteBinding.cs b/osu.Game/Migrations/20180131154205_AddMuteBinding.cs
deleted file mode 100644
index 5564a30bbf..0000000000
--- a/osu.Game/Migrations/20180131154205_AddMuteBinding.cs
+++ /dev/null
@@ -1,23 +0,0 @@
-using Microsoft.EntityFrameworkCore.Migrations;
-using Microsoft.EntityFrameworkCore.Infrastructure;
-using osu.Game.Database;
-using osu.Game.Input.Bindings;
-
-namespace osu.Game.Migrations
-{
-    [DbContext(typeof(OsuDbContext))]
-    [Migration("20180131154205_AddMuteBinding")]
-    public partial class AddMuteBinding : Migration
-    {
-        protected override void Up(MigrationBuilder migrationBuilder)
-        {
-            migrationBuilder.Sql($"UPDATE KeyBinding SET Action = Action + 1 WHERE RulesetID IS NULL AND Variant IS NULL AND Action >= {(int)GlobalAction.ToggleMute}");
-        }
-
-        protected override void Down(MigrationBuilder migrationBuilder)
-        {
-            migrationBuilder.Sql($"DELETE FROM KeyBinding WHERE RulesetID IS NULL AND Variant IS NULL AND Action = {(int)GlobalAction.ToggleMute}");
-            migrationBuilder.Sql($"UPDATE KeyBinding SET Action = Action - 1 WHERE RulesetID IS NULL AND Variant IS NULL AND Action > {(int)GlobalAction.ToggleMute}");
-        }
-    }
-}
diff --git a/osu.Game/Migrations/20180219060912_AddSkins.Designer.cs b/osu.Game/Migrations/20180219060912_AddSkins.Designer.cs
deleted file mode 100644
index cdc4ef2e66..0000000000
--- a/osu.Game/Migrations/20180219060912_AddSkins.Designer.cs
+++ /dev/null
@@ -1,379 +0,0 @@
-// <auto-generated />
-using Microsoft.EntityFrameworkCore;
-using Microsoft.EntityFrameworkCore.Infrastructure;
-using Microsoft.EntityFrameworkCore.Metadata;
-using Microsoft.EntityFrameworkCore.Migrations;
-using Microsoft.EntityFrameworkCore.Storage;
-using osu.Game.Database;
-using System;
-
-namespace osu.Game.Migrations
-{
-    [DbContext(typeof(OsuDbContext))]
-    [Migration("20180219060912_AddSkins")]
-    partial class AddSkins
-    {
-        protected override void BuildTargetModel(ModelBuilder modelBuilder)
-        {
-#pragma warning disable 612, 618
-            modelBuilder
-                .HasAnnotation("ProductVersion", "2.0.0-rtm-26452");
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapDifficulty", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<float>("ApproachRate");
-
-                    b.Property<float>("CircleSize");
-
-                    b.Property<float>("DrainRate");
-
-                    b.Property<float>("OverallDifficulty");
-
-                    b.Property<float>("SliderMultiplier");
-
-                    b.Property<float>("SliderTickRate");
-
-                    b.HasKey("ID");
-
-                    b.ToTable("BeatmapDifficulty");
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<int>("AudioLeadIn");
-
-                    b.Property<int>("BaseDifficultyID");
-
-                    b.Property<int>("BeatDivisor");
-
-                    b.Property<int>("BeatmapSetInfoID");
-
-                    b.Property<bool>("Countdown");
-
-                    b.Property<double>("DistanceSpacing");
-
-                    b.Property<int>("GridSize");
-
-                    b.Property<string>("Hash");
-
-                    b.Property<bool>("Hidden");
-
-                    b.Property<bool>("LetterboxInBreaks");
-
-                    b.Property<string>("MD5Hash");
-
-                    b.Property<int?>("MetadataID");
-
-                    b.Property<int?>("OnlineBeatmapID");
-
-                    b.Property<string>("Path");
-
-                    b.Property<int>("RulesetID");
-
-                    b.Property<bool>("SpecialStyle");
-
-                    b.Property<float>("StackLeniency");
-
-                    b.Property<double>("StarDifficulty");
-
-                    b.Property<string>("StoredBookmarks");
-
-                    b.Property<double>("TimelineZoom");
-
-                    b.Property<string>("Version");
-
-                    b.Property<bool>("WidescreenStoryboard");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("BaseDifficultyID");
-
-                    b.HasIndex("BeatmapSetInfoID");
-
-                    b.HasIndex("Hash")
-                        .IsUnique();
-
-                    b.HasIndex("MD5Hash")
-                        .IsUnique();
-
-                    b.HasIndex("MetadataID");
-
-                    b.HasIndex("OnlineBeatmapID")
-                        .IsUnique();
-
-                    b.HasIndex("RulesetID");
-
-                    b.ToTable("BeatmapInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapMetadata", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<string>("Artist");
-
-                    b.Property<string>("ArtistUnicode");
-
-                    b.Property<string>("AudioFile");
-
-                    b.Property<string>("AuthorString")
-                        .HasColumnName("Author");
-
-                    b.Property<string>("BackgroundFile");
-
-                    b.Property<int>("PreviewTime");
-
-                    b.Property<string>("Source");
-
-                    b.Property<string>("Tags");
-
-                    b.Property<string>("Title");
-
-                    b.Property<string>("TitleUnicode");
-
-                    b.HasKey("ID");
-
-                    b.ToTable("BeatmapMetadata");
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<int>("BeatmapSetInfoID");
-
-                    b.Property<int>("FileInfoID");
-
-                    b.Property<string>("Filename")
-                        .IsRequired();
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("BeatmapSetInfoID");
-
-                    b.HasIndex("FileInfoID");
-
-                    b.ToTable("BeatmapSetFileInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<bool>("DeletePending");
-
-                    b.Property<string>("Hash");
-
-                    b.Property<int?>("MetadataID");
-
-                    b.Property<int?>("OnlineBeatmapSetID");
-
-                    b.Property<bool>("Protected");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("DeletePending");
-
-                    b.HasIndex("Hash")
-                        .IsUnique();
-
-                    b.HasIndex("MetadataID");
-
-                    b.HasIndex("OnlineBeatmapSetID")
-                        .IsUnique();
-
-                    b.ToTable("BeatmapSetInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Configuration.DatabasedSetting", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<int>("IntKey")
-                        .HasColumnName("Key");
-
-                    b.Property<int?>("RulesetID");
-
-                    b.Property<string>("StringValue")
-                        .HasColumnName("Value");
-
-                    b.Property<int?>("Variant");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("RulesetID", "Variant");
-
-                    b.ToTable("Settings");
-                });
-
-            modelBuilder.Entity("osu.Game.Input.Bindings.DatabasedKeyBinding", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<int>("IntAction")
-                        .HasColumnName("Action");
-
-                    b.Property<string>("KeysString")
-                        .HasColumnName("Keys");
-
-                    b.Property<int?>("RulesetID");
-
-                    b.Property<int?>("Variant");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("IntAction");
-
-                    b.HasIndex("RulesetID", "Variant");
-
-                    b.ToTable("KeyBinding");
-                });
-
-            modelBuilder.Entity("osu.Game.IO.FileInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<string>("Hash");
-
-                    b.Property<int>("ReferenceCount");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("Hash")
-                        .IsUnique();
-
-                    b.HasIndex("ReferenceCount");
-
-                    b.ToTable("FileInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Rulesets.RulesetInfo", b =>
-                {
-                    b.Property<int?>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<bool>("Available");
-
-                    b.Property<string>("InstantiationInfo");
-
-                    b.Property<string>("Name");
-
-                    b.Property<string>("ShortName");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("Available");
-
-                    b.HasIndex("ShortName")
-                        .IsUnique();
-
-                    b.ToTable("RulesetInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<int>("FileInfoID");
-
-                    b.Property<string>("Filename")
-                        .IsRequired();
-
-                    b.Property<int>("SkinInfoID");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("FileInfoID");
-
-                    b.HasIndex("SkinInfoID");
-
-                    b.ToTable("SkinFileInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Skinning.SkinInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<string>("Creator");
-
-                    b.Property<bool>("DeletePending");
-
-                    b.Property<string>("Name");
-
-                    b.HasKey("ID");
-
-                    b.ToTable("SkinInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b =>
-                {
-                    b.HasOne("osu.Game.Beatmaps.BeatmapDifficulty", "BaseDifficulty")
-                        .WithMany()
-                        .HasForeignKey("BaseDifficultyID")
-                        .OnDelete(DeleteBehavior.Cascade);
-
-                    b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo", "BeatmapSet")
-                        .WithMany("Beatmaps")
-                        .HasForeignKey("BeatmapSetInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-
-                    b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata")
-                        .WithMany("Beatmaps")
-                        .HasForeignKey("MetadataID");
-
-                    b.HasOne("osu.Game.Rulesets.RulesetInfo", "Ruleset")
-                        .WithMany()
-                        .HasForeignKey("RulesetID")
-                        .OnDelete(DeleteBehavior.Cascade);
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b =>
-                {
-                    b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo")
-                        .WithMany("Files")
-                        .HasForeignKey("BeatmapSetInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-
-                    b.HasOne("osu.Game.IO.FileInfo", "FileInfo")
-                        .WithMany()
-                        .HasForeignKey("FileInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b =>
-                {
-                    b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata")
-                        .WithMany("BeatmapSets")
-                        .HasForeignKey("MetadataID");
-                });
-
-            modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b =>
-                {
-                    b.HasOne("osu.Game.IO.FileInfo", "FileInfo")
-                        .WithMany()
-                        .HasForeignKey("FileInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-
-                    b.HasOne("osu.Game.Skinning.SkinInfo")
-                        .WithMany("Files")
-                        .HasForeignKey("SkinInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-                });
-#pragma warning restore 612, 618
-        }
-    }
-}
diff --git a/osu.Game/Migrations/20180219060912_AddSkins.cs b/osu.Game/Migrations/20180219060912_AddSkins.cs
deleted file mode 100644
index a0270ab0fd..0000000000
--- a/osu.Game/Migrations/20180219060912_AddSkins.cs
+++ /dev/null
@@ -1,71 +0,0 @@
-using Microsoft.EntityFrameworkCore.Migrations;
-
-namespace osu.Game.Migrations
-{
-    public partial class AddSkins : Migration
-    {
-        protected override void Up(MigrationBuilder migrationBuilder)
-        {
-            migrationBuilder.CreateTable(
-                name: "SkinInfo",
-                columns: table => new
-                {
-                    ID = table.Column<int>(type: "INTEGER", nullable: false)
-                        .Annotation("Sqlite:Autoincrement", true),
-                    Creator = table.Column<string>(type: "TEXT", nullable: true),
-                    DeletePending = table.Column<bool>(type: "INTEGER", nullable: false),
-                    Name = table.Column<string>(type: "TEXT", nullable: true)
-                },
-                constraints: table =>
-                {
-                    table.PrimaryKey("PK_SkinInfo", x => x.ID);
-                });
-
-            migrationBuilder.CreateTable(
-                name: "SkinFileInfo",
-                columns: table => new
-                {
-                    ID = table.Column<int>(type: "INTEGER", nullable: false)
-                        .Annotation("Sqlite:Autoincrement", true),
-                    FileInfoID = table.Column<int>(type: "INTEGER", nullable: false),
-                    Filename = table.Column<string>(type: "TEXT", nullable: false),
-                    SkinInfoID = table.Column<int>(type: "INTEGER", nullable: false)
-                },
-                constraints: table =>
-                {
-                    table.PrimaryKey("PK_SkinFileInfo", x => x.ID);
-                    table.ForeignKey(
-                        name: "FK_SkinFileInfo_FileInfo_FileInfoID",
-                        column: x => x.FileInfoID,
-                        principalTable: "FileInfo",
-                        principalColumn: "ID",
-                        onDelete: ReferentialAction.Cascade);
-                    table.ForeignKey(
-                        name: "FK_SkinFileInfo_SkinInfo_SkinInfoID",
-                        column: x => x.SkinInfoID,
-                        principalTable: "SkinInfo",
-                        principalColumn: "ID",
-                        onDelete: ReferentialAction.Cascade);
-                });
-
-            migrationBuilder.CreateIndex(
-                name: "IX_SkinFileInfo_FileInfoID",
-                table: "SkinFileInfo",
-                column: "FileInfoID");
-
-            migrationBuilder.CreateIndex(
-                name: "IX_SkinFileInfo_SkinInfoID",
-                table: "SkinFileInfo",
-                column: "SkinInfoID");
-        }
-
-        protected override void Down(MigrationBuilder migrationBuilder)
-        {
-            migrationBuilder.DropTable(
-                name: "SkinFileInfo");
-
-            migrationBuilder.DropTable(
-                name: "SkinInfo");
-        }
-    }
-}
diff --git a/osu.Game/Migrations/20180529055154_RemoveUniqueHashConstraints.Designer.cs b/osu.Game/Migrations/20180529055154_RemoveUniqueHashConstraints.Designer.cs
deleted file mode 100644
index f28408bfb3..0000000000
--- a/osu.Game/Migrations/20180529055154_RemoveUniqueHashConstraints.Designer.cs
+++ /dev/null
@@ -1,377 +0,0 @@
-// <auto-generated />
-using Microsoft.EntityFrameworkCore;
-using Microsoft.EntityFrameworkCore.Infrastructure;
-using Microsoft.EntityFrameworkCore.Metadata;
-using Microsoft.EntityFrameworkCore.Migrations;
-using Microsoft.EntityFrameworkCore.Storage;
-using osu.Game.Database;
-using System;
-
-namespace osu.Game.Migrations
-{
-    [DbContext(typeof(OsuDbContext))]
-    [Migration("20180529055154_RemoveUniqueHashConstraints")]
-    partial class RemoveUniqueHashConstraints
-    {
-        protected override void BuildTargetModel(ModelBuilder modelBuilder)
-        {
-#pragma warning disable 612, 618
-            modelBuilder
-                .HasAnnotation("ProductVersion", "2.0.3-rtm-10026");
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapDifficulty", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<float>("ApproachRate");
-
-                    b.Property<float>("CircleSize");
-
-                    b.Property<float>("DrainRate");
-
-                    b.Property<float>("OverallDifficulty");
-
-                    b.Property<double>("SliderMultiplier");
-
-                    b.Property<double>("SliderTickRate");
-
-                    b.HasKey("ID");
-
-                    b.ToTable("BeatmapDifficulty");
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<int>("AudioLeadIn");
-
-                    b.Property<int>("BaseDifficultyID");
-
-                    b.Property<int>("BeatDivisor");
-
-                    b.Property<int>("BeatmapSetInfoID");
-
-                    b.Property<bool>("Countdown");
-
-                    b.Property<double>("DistanceSpacing");
-
-                    b.Property<int>("GridSize");
-
-                    b.Property<string>("Hash");
-
-                    b.Property<bool>("Hidden");
-
-                    b.Property<bool>("LetterboxInBreaks");
-
-                    b.Property<string>("MD5Hash");
-
-                    b.Property<int?>("MetadataID");
-
-                    b.Property<int?>("OnlineBeatmapID");
-
-                    b.Property<string>("Path");
-
-                    b.Property<int>("RulesetID");
-
-                    b.Property<bool>("SpecialStyle");
-
-                    b.Property<float>("StackLeniency");
-
-                    b.Property<double>("StarDifficulty");
-
-                    b.Property<string>("StoredBookmarks");
-
-                    b.Property<double>("TimelineZoom");
-
-                    b.Property<string>("Version");
-
-                    b.Property<bool>("WidescreenStoryboard");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("BaseDifficultyID");
-
-                    b.HasIndex("BeatmapSetInfoID");
-
-                    b.HasIndex("Hash");
-
-                    b.HasIndex("MD5Hash");
-
-                    b.HasIndex("MetadataID");
-
-                    b.HasIndex("OnlineBeatmapID")
-                        .IsUnique();
-
-                    b.HasIndex("RulesetID");
-
-                    b.ToTable("BeatmapInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapMetadata", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<string>("Artist");
-
-                    b.Property<string>("ArtistUnicode");
-
-                    b.Property<string>("AudioFile");
-
-                    b.Property<string>("AuthorString")
-                        .HasColumnName("Author");
-
-                    b.Property<string>("BackgroundFile");
-
-                    b.Property<int>("PreviewTime");
-
-                    b.Property<string>("Source");
-
-                    b.Property<string>("Tags");
-
-                    b.Property<string>("Title");
-
-                    b.Property<string>("TitleUnicode");
-
-                    b.HasKey("ID");
-
-                    b.ToTable("BeatmapMetadata");
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<int>("BeatmapSetInfoID");
-
-                    b.Property<int>("FileInfoID");
-
-                    b.Property<string>("Filename")
-                        .IsRequired();
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("BeatmapSetInfoID");
-
-                    b.HasIndex("FileInfoID");
-
-                    b.ToTable("BeatmapSetFileInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<bool>("DeletePending");
-
-                    b.Property<string>("Hash");
-
-                    b.Property<int?>("MetadataID");
-
-                    b.Property<int?>("OnlineBeatmapSetID");
-
-                    b.Property<bool>("Protected");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("DeletePending");
-
-                    b.HasIndex("Hash")
-                        .IsUnique();
-
-                    b.HasIndex("MetadataID");
-
-                    b.HasIndex("OnlineBeatmapSetID")
-                        .IsUnique();
-
-                    b.ToTable("BeatmapSetInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Configuration.DatabasedSetting", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<int>("IntKey")
-                        .HasColumnName("Key");
-
-                    b.Property<int?>("RulesetID");
-
-                    b.Property<string>("StringValue")
-                        .HasColumnName("Value");
-
-                    b.Property<int?>("Variant");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("RulesetID", "Variant");
-
-                    b.ToTable("Settings");
-                });
-
-            modelBuilder.Entity("osu.Game.Input.Bindings.DatabasedKeyBinding", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<int>("IntAction")
-                        .HasColumnName("Action");
-
-                    b.Property<string>("KeysString")
-                        .HasColumnName("Keys");
-
-                    b.Property<int?>("RulesetID");
-
-                    b.Property<int?>("Variant");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("IntAction");
-
-                    b.HasIndex("RulesetID", "Variant");
-
-                    b.ToTable("KeyBinding");
-                });
-
-            modelBuilder.Entity("osu.Game.IO.FileInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<string>("Hash");
-
-                    b.Property<int>("ReferenceCount");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("Hash")
-                        .IsUnique();
-
-                    b.HasIndex("ReferenceCount");
-
-                    b.ToTable("FileInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Rulesets.RulesetInfo", b =>
-                {
-                    b.Property<int?>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<bool>("Available");
-
-                    b.Property<string>("InstantiationInfo");
-
-                    b.Property<string>("Name");
-
-                    b.Property<string>("ShortName");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("Available");
-
-                    b.HasIndex("ShortName")
-                        .IsUnique();
-
-                    b.ToTable("RulesetInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<int>("FileInfoID");
-
-                    b.Property<string>("Filename")
-                        .IsRequired();
-
-                    b.Property<int>("SkinInfoID");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("FileInfoID");
-
-                    b.HasIndex("SkinInfoID");
-
-                    b.ToTable("SkinFileInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Skinning.SkinInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<string>("Creator");
-
-                    b.Property<bool>("DeletePending");
-
-                    b.Property<string>("Name");
-
-                    b.HasKey("ID");
-
-                    b.ToTable("SkinInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b =>
-                {
-                    b.HasOne("osu.Game.Beatmaps.BeatmapDifficulty", "BaseDifficulty")
-                        .WithMany()
-                        .HasForeignKey("BaseDifficultyID")
-                        .OnDelete(DeleteBehavior.Cascade);
-
-                    b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo", "BeatmapSet")
-                        .WithMany("Beatmaps")
-                        .HasForeignKey("BeatmapSetInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-
-                    b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata")
-                        .WithMany("Beatmaps")
-                        .HasForeignKey("MetadataID");
-
-                    b.HasOne("osu.Game.Rulesets.RulesetInfo", "Ruleset")
-                        .WithMany()
-                        .HasForeignKey("RulesetID")
-                        .OnDelete(DeleteBehavior.Cascade);
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b =>
-                {
-                    b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo")
-                        .WithMany("Files")
-                        .HasForeignKey("BeatmapSetInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-
-                    b.HasOne("osu.Game.IO.FileInfo", "FileInfo")
-                        .WithMany()
-                        .HasForeignKey("FileInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b =>
-                {
-                    b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata")
-                        .WithMany("BeatmapSets")
-                        .HasForeignKey("MetadataID");
-                });
-
-            modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b =>
-                {
-                    b.HasOne("osu.Game.IO.FileInfo", "FileInfo")
-                        .WithMany()
-                        .HasForeignKey("FileInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-
-                    b.HasOne("osu.Game.Skinning.SkinInfo")
-                        .WithMany("Files")
-                        .HasForeignKey("SkinInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-                });
-#pragma warning restore 612, 618
-        }
-    }
-}
diff --git a/osu.Game/Migrations/20180529055154_RemoveUniqueHashConstraints.cs b/osu.Game/Migrations/20180529055154_RemoveUniqueHashConstraints.cs
deleted file mode 100644
index 27269cc5fc..0000000000
--- a/osu.Game/Migrations/20180529055154_RemoveUniqueHashConstraints.cs
+++ /dev/null
@@ -1,51 +0,0 @@
-using Microsoft.EntityFrameworkCore.Migrations;
-
-namespace osu.Game.Migrations
-{
-    public partial class RemoveUniqueHashConstraints : Migration
-    {
-        protected override void Up(MigrationBuilder migrationBuilder)
-        {
-            migrationBuilder.DropIndex(
-                name: "IX_BeatmapInfo_Hash",
-                table: "BeatmapInfo");
-
-            migrationBuilder.DropIndex(
-                name: "IX_BeatmapInfo_MD5Hash",
-                table: "BeatmapInfo");
-
-            migrationBuilder.CreateIndex(
-                name: "IX_BeatmapInfo_Hash",
-                table: "BeatmapInfo",
-                column: "Hash");
-
-            migrationBuilder.CreateIndex(
-                name: "IX_BeatmapInfo_MD5Hash",
-                table: "BeatmapInfo",
-                column: "MD5Hash");
-        }
-
-        protected override void Down(MigrationBuilder migrationBuilder)
-        {
-            migrationBuilder.DropIndex(
-                name: "IX_BeatmapInfo_Hash",
-                table: "BeatmapInfo");
-
-            migrationBuilder.DropIndex(
-                name: "IX_BeatmapInfo_MD5Hash",
-                table: "BeatmapInfo");
-
-            migrationBuilder.CreateIndex(
-                name: "IX_BeatmapInfo_Hash",
-                table: "BeatmapInfo",
-                column: "Hash",
-                unique: true);
-
-            migrationBuilder.CreateIndex(
-                name: "IX_BeatmapInfo_MD5Hash",
-                table: "BeatmapInfo",
-                column: "MD5Hash",
-                unique: true);
-        }
-    }
-}
diff --git a/osu.Game/Migrations/20180621044111_UpdateTaikoDefaultBindings.Designer.cs b/osu.Game/Migrations/20180621044111_UpdateTaikoDefaultBindings.Designer.cs
deleted file mode 100644
index aaa11e88b6..0000000000
--- a/osu.Game/Migrations/20180621044111_UpdateTaikoDefaultBindings.Designer.cs
+++ /dev/null
@@ -1,376 +0,0 @@
-// <auto-generated />
-using System;
-using Microsoft.EntityFrameworkCore;
-using Microsoft.EntityFrameworkCore.Infrastructure;
-using Microsoft.EntityFrameworkCore.Migrations;
-using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
-using osu.Game.Database;
-
-namespace osu.Game.Migrations
-{
-    [DbContext(typeof(OsuDbContext))]
-    [Migration("20180621044111_UpdateTaikoDefaultBindings")]
-    partial class UpdateTaikoDefaultBindings
-    {
-        protected override void BuildTargetModel(ModelBuilder modelBuilder)
-        {
-#pragma warning disable 612, 618
-            modelBuilder
-                .HasAnnotation("ProductVersion", "2.1.1-rtm-30846");
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapDifficulty", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<float>("ApproachRate");
-
-                    b.Property<float>("CircleSize");
-
-                    b.Property<float>("DrainRate");
-
-                    b.Property<float>("OverallDifficulty");
-
-                    b.Property<double>("SliderMultiplier");
-
-                    b.Property<double>("SliderTickRate");
-
-                    b.HasKey("ID");
-
-                    b.ToTable("BeatmapDifficulty");
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<int>("AudioLeadIn");
-
-                    b.Property<int>("BaseDifficultyID");
-
-                    b.Property<int>("BeatDivisor");
-
-                    b.Property<int>("BeatmapSetInfoID");
-
-                    b.Property<bool>("Countdown");
-
-                    b.Property<double>("DistanceSpacing");
-
-                    b.Property<int>("GridSize");
-
-                    b.Property<string>("Hash");
-
-                    b.Property<bool>("Hidden");
-
-                    b.Property<bool>("LetterboxInBreaks");
-
-                    b.Property<string>("MD5Hash");
-
-                    b.Property<int?>("MetadataID");
-
-                    b.Property<int?>("OnlineBeatmapID");
-
-                    b.Property<string>("Path");
-
-                    b.Property<int>("RulesetID");
-
-                    b.Property<bool>("SpecialStyle");
-
-                    b.Property<float>("StackLeniency");
-
-                    b.Property<double>("StarDifficulty");
-
-                    b.Property<string>("StoredBookmarks");
-
-                    b.Property<double>("TimelineZoom");
-
-                    b.Property<string>("Version");
-
-                    b.Property<bool>("WidescreenStoryboard");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("BaseDifficultyID");
-
-                    b.HasIndex("BeatmapSetInfoID");
-
-                    b.HasIndex("Hash");
-
-                    b.HasIndex("MD5Hash");
-
-                    b.HasIndex("MetadataID");
-
-                    b.HasIndex("OnlineBeatmapID")
-                        .IsUnique();
-
-                    b.HasIndex("RulesetID");
-
-                    b.ToTable("BeatmapInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapMetadata", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<string>("Artist");
-
-                    b.Property<string>("ArtistUnicode");
-
-                    b.Property<string>("AudioFile");
-
-                    b.Property<string>("AuthorString")
-                        .HasColumnName("Author");
-
-                    b.Property<string>("BackgroundFile");
-
-                    b.Property<int>("PreviewTime");
-
-                    b.Property<string>("Source");
-
-                    b.Property<string>("Tags");
-
-                    b.Property<string>("Title");
-
-                    b.Property<string>("TitleUnicode");
-
-                    b.HasKey("ID");
-
-                    b.ToTable("BeatmapMetadata");
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<int>("BeatmapSetInfoID");
-
-                    b.Property<int>("FileInfoID");
-
-                    b.Property<string>("Filename")
-                        .IsRequired();
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("BeatmapSetInfoID");
-
-                    b.HasIndex("FileInfoID");
-
-                    b.ToTable("BeatmapSetFileInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<bool>("DeletePending");
-
-                    b.Property<string>("Hash");
-
-                    b.Property<int?>("MetadataID");
-
-                    b.Property<int?>("OnlineBeatmapSetID");
-
-                    b.Property<bool>("Protected");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("DeletePending");
-
-                    b.HasIndex("Hash")
-                        .IsUnique();
-
-                    b.HasIndex("MetadataID");
-
-                    b.HasIndex("OnlineBeatmapSetID")
-                        .IsUnique();
-
-                    b.ToTable("BeatmapSetInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Configuration.DatabasedSetting", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<int>("IntKey")
-                        .HasColumnName("Key");
-
-                    b.Property<int?>("RulesetID");
-
-                    b.Property<string>("StringValue")
-                        .HasColumnName("Value");
-
-                    b.Property<int?>("Variant");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("RulesetID", "Variant");
-
-                    b.ToTable("Settings");
-                });
-
-            modelBuilder.Entity("osu.Game.Input.Bindings.DatabasedKeyBinding", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<int>("IntAction")
-                        .HasColumnName("Action");
-
-                    b.Property<string>("KeysString")
-                        .HasColumnName("Keys");
-
-                    b.Property<int?>("RulesetID");
-
-                    b.Property<int?>("Variant");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("IntAction");
-
-                    b.HasIndex("RulesetID", "Variant");
-
-                    b.ToTable("KeyBinding");
-                });
-
-            modelBuilder.Entity("osu.Game.IO.FileInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<string>("Hash");
-
-                    b.Property<int>("ReferenceCount");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("Hash")
-                        .IsUnique();
-
-                    b.HasIndex("ReferenceCount");
-
-                    b.ToTable("FileInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Rulesets.RulesetInfo", b =>
-                {
-                    b.Property<int?>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<bool>("Available");
-
-                    b.Property<string>("InstantiationInfo");
-
-                    b.Property<string>("Name");
-
-                    b.Property<string>("ShortName");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("Available");
-
-                    b.HasIndex("ShortName")
-                        .IsUnique();
-
-                    b.ToTable("RulesetInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<int>("FileInfoID");
-
-                    b.Property<string>("Filename")
-                        .IsRequired();
-
-                    b.Property<int>("SkinInfoID");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("FileInfoID");
-
-                    b.HasIndex("SkinInfoID");
-
-                    b.ToTable("SkinFileInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Skinning.SkinInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<string>("Creator");
-
-                    b.Property<bool>("DeletePending");
-
-                    b.Property<string>("Name");
-
-                    b.HasKey("ID");
-
-                    b.ToTable("SkinInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b =>
-                {
-                    b.HasOne("osu.Game.Beatmaps.BeatmapDifficulty", "BaseDifficulty")
-                        .WithMany()
-                        .HasForeignKey("BaseDifficultyID")
-                        .OnDelete(DeleteBehavior.Cascade);
-
-                    b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo", "BeatmapSet")
-                        .WithMany("Beatmaps")
-                        .HasForeignKey("BeatmapSetInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-
-                    b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata")
-                        .WithMany("Beatmaps")
-                        .HasForeignKey("MetadataID");
-
-                    b.HasOne("osu.Game.Rulesets.RulesetInfo", "Ruleset")
-                        .WithMany()
-                        .HasForeignKey("RulesetID")
-                        .OnDelete(DeleteBehavior.Cascade);
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b =>
-                {
-                    b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo")
-                        .WithMany("Files")
-                        .HasForeignKey("BeatmapSetInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-
-                    b.HasOne("osu.Game.IO.FileInfo", "FileInfo")
-                        .WithMany()
-                        .HasForeignKey("FileInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b =>
-                {
-                    b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata")
-                        .WithMany("BeatmapSets")
-                        .HasForeignKey("MetadataID");
-                });
-
-            modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b =>
-                {
-                    b.HasOne("osu.Game.IO.FileInfo", "FileInfo")
-                        .WithMany()
-                        .HasForeignKey("FileInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-
-                    b.HasOne("osu.Game.Skinning.SkinInfo")
-                        .WithMany("Files")
-                        .HasForeignKey("SkinInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-                });
-#pragma warning restore 612, 618
-        }
-    }
-}
diff --git a/osu.Game/Migrations/20180621044111_UpdateTaikoDefaultBindings.cs b/osu.Game/Migrations/20180621044111_UpdateTaikoDefaultBindings.cs
deleted file mode 100644
index 71304ea979..0000000000
--- a/osu.Game/Migrations/20180621044111_UpdateTaikoDefaultBindings.cs
+++ /dev/null
@@ -1,17 +0,0 @@
-using Microsoft.EntityFrameworkCore.Migrations;
-
-namespace osu.Game.Migrations
-{
-    public partial class UpdateTaikoDefaultBindings : Migration
-    {
-        protected override void Up(MigrationBuilder migrationBuilder)
-        {
-            migrationBuilder.Sql("DELETE FROM KeyBinding WHERE RulesetID = 1");
-        }
-
-        protected override void Down(MigrationBuilder migrationBuilder)
-        {
-            // we can't really tell if these should be restored or not, so let's just not do so.
-        }
-    }
-}
diff --git a/osu.Game/Migrations/20180628011956_RemoveNegativeSetIDs.Designer.cs b/osu.Game/Migrations/20180628011956_RemoveNegativeSetIDs.Designer.cs
deleted file mode 100644
index 7eeacd56d7..0000000000
--- a/osu.Game/Migrations/20180628011956_RemoveNegativeSetIDs.Designer.cs
+++ /dev/null
@@ -1,376 +0,0 @@
-// <auto-generated />
-using System;
-using Microsoft.EntityFrameworkCore;
-using Microsoft.EntityFrameworkCore.Infrastructure;
-using Microsoft.EntityFrameworkCore.Migrations;
-using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
-using osu.Game.Database;
-
-namespace osu.Game.Migrations
-{
-    [DbContext(typeof(OsuDbContext))]
-    [Migration("20180628011956_RemoveNegativeSetIDs")]
-    partial class RemoveNegativeSetIDs
-    {
-        protected override void BuildTargetModel(ModelBuilder modelBuilder)
-        {
-#pragma warning disable 612, 618
-            modelBuilder
-                .HasAnnotation("ProductVersion", "2.1.1-rtm-30846");
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapDifficulty", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<float>("ApproachRate");
-
-                    b.Property<float>("CircleSize");
-
-                    b.Property<float>("DrainRate");
-
-                    b.Property<float>("OverallDifficulty");
-
-                    b.Property<double>("SliderMultiplier");
-
-                    b.Property<double>("SliderTickRate");
-
-                    b.HasKey("ID");
-
-                    b.ToTable("BeatmapDifficulty");
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<int>("AudioLeadIn");
-
-                    b.Property<int>("BaseDifficultyID");
-
-                    b.Property<int>("BeatDivisor");
-
-                    b.Property<int>("BeatmapSetInfoID");
-
-                    b.Property<bool>("Countdown");
-
-                    b.Property<double>("DistanceSpacing");
-
-                    b.Property<int>("GridSize");
-
-                    b.Property<string>("Hash");
-
-                    b.Property<bool>("Hidden");
-
-                    b.Property<bool>("LetterboxInBreaks");
-
-                    b.Property<string>("MD5Hash");
-
-                    b.Property<int?>("MetadataID");
-
-                    b.Property<int?>("OnlineBeatmapID");
-
-                    b.Property<string>("Path");
-
-                    b.Property<int>("RulesetID");
-
-                    b.Property<bool>("SpecialStyle");
-
-                    b.Property<float>("StackLeniency");
-
-                    b.Property<double>("StarDifficulty");
-
-                    b.Property<string>("StoredBookmarks");
-
-                    b.Property<double>("TimelineZoom");
-
-                    b.Property<string>("Version");
-
-                    b.Property<bool>("WidescreenStoryboard");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("BaseDifficultyID");
-
-                    b.HasIndex("BeatmapSetInfoID");
-
-                    b.HasIndex("Hash");
-
-                    b.HasIndex("MD5Hash");
-
-                    b.HasIndex("MetadataID");
-
-                    b.HasIndex("OnlineBeatmapID")
-                        .IsUnique();
-
-                    b.HasIndex("RulesetID");
-
-                    b.ToTable("BeatmapInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapMetadata", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<string>("Artist");
-
-                    b.Property<string>("ArtistUnicode");
-
-                    b.Property<string>("AudioFile");
-
-                    b.Property<string>("AuthorString")
-                        .HasColumnName("Author");
-
-                    b.Property<string>("BackgroundFile");
-
-                    b.Property<int>("PreviewTime");
-
-                    b.Property<string>("Source");
-
-                    b.Property<string>("Tags");
-
-                    b.Property<string>("Title");
-
-                    b.Property<string>("TitleUnicode");
-
-                    b.HasKey("ID");
-
-                    b.ToTable("BeatmapMetadata");
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<int>("BeatmapSetInfoID");
-
-                    b.Property<int>("FileInfoID");
-
-                    b.Property<string>("Filename")
-                        .IsRequired();
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("BeatmapSetInfoID");
-
-                    b.HasIndex("FileInfoID");
-
-                    b.ToTable("BeatmapSetFileInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<bool>("DeletePending");
-
-                    b.Property<string>("Hash");
-
-                    b.Property<int?>("MetadataID");
-
-                    b.Property<int?>("OnlineBeatmapSetID");
-
-                    b.Property<bool>("Protected");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("DeletePending");
-
-                    b.HasIndex("Hash")
-                        .IsUnique();
-
-                    b.HasIndex("MetadataID");
-
-                    b.HasIndex("OnlineBeatmapSetID")
-                        .IsUnique();
-
-                    b.ToTable("BeatmapSetInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Configuration.DatabasedSetting", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<int>("IntKey")
-                        .HasColumnName("Key");
-
-                    b.Property<int?>("RulesetID");
-
-                    b.Property<string>("StringValue")
-                        .HasColumnName("Value");
-
-                    b.Property<int?>("Variant");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("RulesetID", "Variant");
-
-                    b.ToTable("Settings");
-                });
-
-            modelBuilder.Entity("osu.Game.Input.Bindings.DatabasedKeyBinding", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<int>("IntAction")
-                        .HasColumnName("Action");
-
-                    b.Property<string>("KeysString")
-                        .HasColumnName("Keys");
-
-                    b.Property<int?>("RulesetID");
-
-                    b.Property<int?>("Variant");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("IntAction");
-
-                    b.HasIndex("RulesetID", "Variant");
-
-                    b.ToTable("KeyBinding");
-                });
-
-            modelBuilder.Entity("osu.Game.IO.FileInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<string>("Hash");
-
-                    b.Property<int>("ReferenceCount");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("Hash")
-                        .IsUnique();
-
-                    b.HasIndex("ReferenceCount");
-
-                    b.ToTable("FileInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Rulesets.RulesetInfo", b =>
-                {
-                    b.Property<int?>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<bool>("Available");
-
-                    b.Property<string>("InstantiationInfo");
-
-                    b.Property<string>("Name");
-
-                    b.Property<string>("ShortName");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("Available");
-
-                    b.HasIndex("ShortName")
-                        .IsUnique();
-
-                    b.ToTable("RulesetInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<int>("FileInfoID");
-
-                    b.Property<string>("Filename")
-                        .IsRequired();
-
-                    b.Property<int>("SkinInfoID");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("FileInfoID");
-
-                    b.HasIndex("SkinInfoID");
-
-                    b.ToTable("SkinFileInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Skinning.SkinInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<string>("Creator");
-
-                    b.Property<bool>("DeletePending");
-
-                    b.Property<string>("Name");
-
-                    b.HasKey("ID");
-
-                    b.ToTable("SkinInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b =>
-                {
-                    b.HasOne("osu.Game.Beatmaps.BeatmapDifficulty", "BaseDifficulty")
-                        .WithMany()
-                        .HasForeignKey("BaseDifficultyID")
-                        .OnDelete(DeleteBehavior.Cascade);
-
-                    b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo", "BeatmapSet")
-                        .WithMany("Beatmaps")
-                        .HasForeignKey("BeatmapSetInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-
-                    b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata")
-                        .WithMany("Beatmaps")
-                        .HasForeignKey("MetadataID");
-
-                    b.HasOne("osu.Game.Rulesets.RulesetInfo", "Ruleset")
-                        .WithMany()
-                        .HasForeignKey("RulesetID")
-                        .OnDelete(DeleteBehavior.Cascade);
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b =>
-                {
-                    b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo")
-                        .WithMany("Files")
-                        .HasForeignKey("BeatmapSetInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-
-                    b.HasOne("osu.Game.IO.FileInfo", "FileInfo")
-                        .WithMany()
-                        .HasForeignKey("FileInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b =>
-                {
-                    b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata")
-                        .WithMany("BeatmapSets")
-                        .HasForeignKey("MetadataID");
-                });
-
-            modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b =>
-                {
-                    b.HasOne("osu.Game.IO.FileInfo", "FileInfo")
-                        .WithMany()
-                        .HasForeignKey("FileInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-
-                    b.HasOne("osu.Game.Skinning.SkinInfo")
-                        .WithMany("Files")
-                        .HasForeignKey("SkinInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-                });
-#pragma warning restore 612, 618
-        }
-    }
-}
diff --git a/osu.Game/Migrations/20180628011956_RemoveNegativeSetIDs.cs b/osu.Game/Migrations/20180628011956_RemoveNegativeSetIDs.cs
deleted file mode 100644
index 506d65f761..0000000000
--- a/osu.Game/Migrations/20180628011956_RemoveNegativeSetIDs.cs
+++ /dev/null
@@ -1,18 +0,0 @@
-using Microsoft.EntityFrameworkCore.Migrations;
-
-namespace osu.Game.Migrations
-{
-    public partial class RemoveNegativeSetIDs : Migration
-    {
-        protected override void Up(MigrationBuilder migrationBuilder)
-        {
-            // There was a change that beatmaps were being loaded with "-1" online IDs, which is completely incorrect.
-            // This ensures there will not be unique key conflicts as a result of these incorrectly imported beatmaps.
-            migrationBuilder.Sql("UPDATE BeatmapSetInfo SET OnlineBeatmapSetID = null WHERE OnlineBeatmapSetID <= 0");
-        }
-
-        protected override void Down(MigrationBuilder migrationBuilder)
-        {
-        }
-    }
-}
diff --git a/osu.Game/Migrations/20180913080842_AddRankStatus.Designer.cs b/osu.Game/Migrations/20180913080842_AddRankStatus.Designer.cs
deleted file mode 100644
index 5ab43da046..0000000000
--- a/osu.Game/Migrations/20180913080842_AddRankStatus.Designer.cs
+++ /dev/null
@@ -1,380 +0,0 @@
-// <auto-generated />
-using System;
-using Microsoft.EntityFrameworkCore;
-using Microsoft.EntityFrameworkCore.Infrastructure;
-using Microsoft.EntityFrameworkCore.Migrations;
-using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
-using osu.Game.Database;
-
-namespace osu.Game.Migrations
-{
-    [DbContext(typeof(OsuDbContext))]
-    [Migration("20180913080842_AddRankStatus")]
-    partial class AddRankStatus
-    {
-        protected override void BuildTargetModel(ModelBuilder modelBuilder)
-        {
-#pragma warning disable 612, 618
-            modelBuilder
-                .HasAnnotation("ProductVersion", "2.1.2-rtm-30932");
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapDifficulty", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<float>("ApproachRate");
-
-                    b.Property<float>("CircleSize");
-
-                    b.Property<float>("DrainRate");
-
-                    b.Property<float>("OverallDifficulty");
-
-                    b.Property<double>("SliderMultiplier");
-
-                    b.Property<double>("SliderTickRate");
-
-                    b.HasKey("ID");
-
-                    b.ToTable("BeatmapDifficulty");
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<int>("AudioLeadIn");
-
-                    b.Property<int>("BaseDifficultyID");
-
-                    b.Property<int>("BeatDivisor");
-
-                    b.Property<int>("BeatmapSetInfoID");
-
-                    b.Property<bool>("Countdown");
-
-                    b.Property<double>("DistanceSpacing");
-
-                    b.Property<int>("GridSize");
-
-                    b.Property<string>("Hash");
-
-                    b.Property<bool>("Hidden");
-
-                    b.Property<bool>("LetterboxInBreaks");
-
-                    b.Property<string>("MD5Hash");
-
-                    b.Property<int?>("MetadataID");
-
-                    b.Property<int?>("OnlineBeatmapID");
-
-                    b.Property<string>("Path");
-
-                    b.Property<int>("RulesetID");
-
-                    b.Property<bool>("SpecialStyle");
-
-                    b.Property<float>("StackLeniency");
-
-                    b.Property<double>("StarDifficulty");
-
-                    b.Property<int>("Status");
-
-                    b.Property<string>("StoredBookmarks");
-
-                    b.Property<double>("TimelineZoom");
-
-                    b.Property<string>("Version");
-
-                    b.Property<bool>("WidescreenStoryboard");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("BaseDifficultyID");
-
-                    b.HasIndex("BeatmapSetInfoID");
-
-                    b.HasIndex("Hash");
-
-                    b.HasIndex("MD5Hash");
-
-                    b.HasIndex("MetadataID");
-
-                    b.HasIndex("OnlineBeatmapID")
-                        .IsUnique();
-
-                    b.HasIndex("RulesetID");
-
-                    b.ToTable("BeatmapInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapMetadata", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<string>("Artist");
-
-                    b.Property<string>("ArtistUnicode");
-
-                    b.Property<string>("AudioFile");
-
-                    b.Property<string>("AuthorString")
-                        .HasColumnName("Author");
-
-                    b.Property<string>("BackgroundFile");
-
-                    b.Property<int>("PreviewTime");
-
-                    b.Property<string>("Source");
-
-                    b.Property<string>("Tags");
-
-                    b.Property<string>("Title");
-
-                    b.Property<string>("TitleUnicode");
-
-                    b.HasKey("ID");
-
-                    b.ToTable("BeatmapMetadata");
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<int>("BeatmapSetInfoID");
-
-                    b.Property<int>("FileInfoID");
-
-                    b.Property<string>("Filename")
-                        .IsRequired();
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("BeatmapSetInfoID");
-
-                    b.HasIndex("FileInfoID");
-
-                    b.ToTable("BeatmapSetFileInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<bool>("DeletePending");
-
-                    b.Property<string>("Hash");
-
-                    b.Property<int?>("MetadataID");
-
-                    b.Property<int?>("OnlineBeatmapSetID");
-
-                    b.Property<bool>("Protected");
-
-                    b.Property<int>("Status");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("DeletePending");
-
-                    b.HasIndex("Hash")
-                        .IsUnique();
-
-                    b.HasIndex("MetadataID");
-
-                    b.HasIndex("OnlineBeatmapSetID")
-                        .IsUnique();
-
-                    b.ToTable("BeatmapSetInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Configuration.DatabasedSetting", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<int>("IntKey")
-                        .HasColumnName("Key");
-
-                    b.Property<int?>("RulesetID");
-
-                    b.Property<string>("StringValue")
-                        .HasColumnName("Value");
-
-                    b.Property<int?>("Variant");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("RulesetID", "Variant");
-
-                    b.ToTable("Settings");
-                });
-
-            modelBuilder.Entity("osu.Game.Input.Bindings.DatabasedKeyBinding", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<int>("IntAction")
-                        .HasColumnName("Action");
-
-                    b.Property<string>("KeysString")
-                        .HasColumnName("Keys");
-
-                    b.Property<int?>("RulesetID");
-
-                    b.Property<int?>("Variant");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("IntAction");
-
-                    b.HasIndex("RulesetID", "Variant");
-
-                    b.ToTable("KeyBinding");
-                });
-
-            modelBuilder.Entity("osu.Game.IO.FileInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<string>("Hash");
-
-                    b.Property<int>("ReferenceCount");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("Hash")
-                        .IsUnique();
-
-                    b.HasIndex("ReferenceCount");
-
-                    b.ToTable("FileInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Rulesets.RulesetInfo", b =>
-                {
-                    b.Property<int?>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<bool>("Available");
-
-                    b.Property<string>("InstantiationInfo");
-
-                    b.Property<string>("Name");
-
-                    b.Property<string>("ShortName");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("Available");
-
-                    b.HasIndex("ShortName")
-                        .IsUnique();
-
-                    b.ToTable("RulesetInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<int>("FileInfoID");
-
-                    b.Property<string>("Filename")
-                        .IsRequired();
-
-                    b.Property<int>("SkinInfoID");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("FileInfoID");
-
-                    b.HasIndex("SkinInfoID");
-
-                    b.ToTable("SkinFileInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Skinning.SkinInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<string>("Creator");
-
-                    b.Property<bool>("DeletePending");
-
-                    b.Property<string>("Name");
-
-                    b.HasKey("ID");
-
-                    b.ToTable("SkinInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b =>
-                {
-                    b.HasOne("osu.Game.Beatmaps.BeatmapDifficulty", "BaseDifficulty")
-                        .WithMany()
-                        .HasForeignKey("BaseDifficultyID")
-                        .OnDelete(DeleteBehavior.Cascade);
-
-                    b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo", "BeatmapSet")
-                        .WithMany("Beatmaps")
-                        .HasForeignKey("BeatmapSetInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-
-                    b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata")
-                        .WithMany("Beatmaps")
-                        .HasForeignKey("MetadataID");
-
-                    b.HasOne("osu.Game.Rulesets.RulesetInfo", "Ruleset")
-                        .WithMany()
-                        .HasForeignKey("RulesetID")
-                        .OnDelete(DeleteBehavior.Cascade);
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b =>
-                {
-                    b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo")
-                        .WithMany("Files")
-                        .HasForeignKey("BeatmapSetInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-
-                    b.HasOne("osu.Game.IO.FileInfo", "FileInfo")
-                        .WithMany()
-                        .HasForeignKey("FileInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b =>
-                {
-                    b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata")
-                        .WithMany("BeatmapSets")
-                        .HasForeignKey("MetadataID");
-                });
-
-            modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b =>
-                {
-                    b.HasOne("osu.Game.IO.FileInfo", "FileInfo")
-                        .WithMany()
-                        .HasForeignKey("FileInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-
-                    b.HasOne("osu.Game.Skinning.SkinInfo")
-                        .WithMany("Files")
-                        .HasForeignKey("SkinInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-                });
-#pragma warning restore 612, 618
-        }
-    }
-}
diff --git a/osu.Game/Migrations/20180913080842_AddRankStatus.cs b/osu.Game/Migrations/20180913080842_AddRankStatus.cs
deleted file mode 100644
index bba4944bb7..0000000000
--- a/osu.Game/Migrations/20180913080842_AddRankStatus.cs
+++ /dev/null
@@ -1,33 +0,0 @@
-using Microsoft.EntityFrameworkCore.Migrations;
-
-namespace osu.Game.Migrations
-{
-    public partial class AddRankStatus : Migration
-    {
-        protected override void Up(MigrationBuilder migrationBuilder)
-        {
-            migrationBuilder.AddColumn<int>(
-                name: "Status",
-                table: "BeatmapSetInfo",
-                nullable: false,
-                defaultValue: -3); // NONE
-
-            migrationBuilder.AddColumn<int>(
-                name: "Status",
-                table: "BeatmapInfo",
-                nullable: false,
-                defaultValue: -3); // NONE
-        }
-
-        protected override void Down(MigrationBuilder migrationBuilder)
-        {
-            migrationBuilder.DropColumn(
-                name: "Status",
-                table: "BeatmapSetInfo");
-
-            migrationBuilder.DropColumn(
-                name: "Status",
-                table: "BeatmapInfo");
-        }
-    }
-}
diff --git a/osu.Game/Migrations/20181007180454_StandardizePaths.Designer.cs b/osu.Game/Migrations/20181007180454_StandardizePaths.Designer.cs
deleted file mode 100644
index b387a45ecf..0000000000
--- a/osu.Game/Migrations/20181007180454_StandardizePaths.Designer.cs
+++ /dev/null
@@ -1,380 +0,0 @@
-// <auto-generated />
-using System;
-using Microsoft.EntityFrameworkCore;
-using Microsoft.EntityFrameworkCore.Infrastructure;
-using Microsoft.EntityFrameworkCore.Migrations;
-using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
-using osu.Game.Database;
-
-namespace osu.Game.Migrations
-{
-    [DbContext(typeof(OsuDbContext))]
-    [Migration("20181007180454_StandardizePaths")]
-    partial class StandardizePaths
-    {
-        protected override void BuildTargetModel(ModelBuilder modelBuilder)
-        {
-#pragma warning disable 612, 618
-            modelBuilder
-                .HasAnnotation("ProductVersion", "2.1.3-rtm-32065");
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapDifficulty", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<float>("ApproachRate");
-
-                    b.Property<float>("CircleSize");
-
-                    b.Property<float>("DrainRate");
-
-                    b.Property<float>("OverallDifficulty");
-
-                    b.Property<double>("SliderMultiplier");
-
-                    b.Property<double>("SliderTickRate");
-
-                    b.HasKey("ID");
-
-                    b.ToTable("BeatmapDifficulty");
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<int>("AudioLeadIn");
-
-                    b.Property<int>("BaseDifficultyID");
-
-                    b.Property<int>("BeatDivisor");
-
-                    b.Property<int>("BeatmapSetInfoID");
-
-                    b.Property<bool>("Countdown");
-
-                    b.Property<double>("DistanceSpacing");
-
-                    b.Property<int>("GridSize");
-
-                    b.Property<string>("Hash");
-
-                    b.Property<bool>("Hidden");
-
-                    b.Property<bool>("LetterboxInBreaks");
-
-                    b.Property<string>("MD5Hash");
-
-                    b.Property<int?>("MetadataID");
-
-                    b.Property<int?>("OnlineBeatmapID");
-
-                    b.Property<string>("Path");
-
-                    b.Property<int>("RulesetID");
-
-                    b.Property<bool>("SpecialStyle");
-
-                    b.Property<float>("StackLeniency");
-
-                    b.Property<double>("StarDifficulty");
-
-                    b.Property<int>("Status");
-
-                    b.Property<string>("StoredBookmarks");
-
-                    b.Property<double>("TimelineZoom");
-
-                    b.Property<string>("Version");
-
-                    b.Property<bool>("WidescreenStoryboard");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("BaseDifficultyID");
-
-                    b.HasIndex("BeatmapSetInfoID");
-
-                    b.HasIndex("Hash");
-
-                    b.HasIndex("MD5Hash");
-
-                    b.HasIndex("MetadataID");
-
-                    b.HasIndex("OnlineBeatmapID")
-                        .IsUnique();
-
-                    b.HasIndex("RulesetID");
-
-                    b.ToTable("BeatmapInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapMetadata", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<string>("Artist");
-
-                    b.Property<string>("ArtistUnicode");
-
-                    b.Property<string>("AudioFile");
-
-                    b.Property<string>("AuthorString")
-                        .HasColumnName("Author");
-
-                    b.Property<string>("BackgroundFile");
-
-                    b.Property<int>("PreviewTime");
-
-                    b.Property<string>("Source");
-
-                    b.Property<string>("Tags");
-
-                    b.Property<string>("Title");
-
-                    b.Property<string>("TitleUnicode");
-
-                    b.HasKey("ID");
-
-                    b.ToTable("BeatmapMetadata");
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<int>("BeatmapSetInfoID");
-
-                    b.Property<int>("FileInfoID");
-
-                    b.Property<string>("Filename")
-                        .IsRequired();
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("BeatmapSetInfoID");
-
-                    b.HasIndex("FileInfoID");
-
-                    b.ToTable("BeatmapSetFileInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<bool>("DeletePending");
-
-                    b.Property<string>("Hash");
-
-                    b.Property<int?>("MetadataID");
-
-                    b.Property<int?>("OnlineBeatmapSetID");
-
-                    b.Property<bool>("Protected");
-
-                    b.Property<int>("Status");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("DeletePending");
-
-                    b.HasIndex("Hash")
-                        .IsUnique();
-
-                    b.HasIndex("MetadataID");
-
-                    b.HasIndex("OnlineBeatmapSetID")
-                        .IsUnique();
-
-                    b.ToTable("BeatmapSetInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Configuration.DatabasedSetting", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<int>("IntKey")
-                        .HasColumnName("Key");
-
-                    b.Property<int?>("RulesetID");
-
-                    b.Property<string>("StringValue")
-                        .HasColumnName("Value");
-
-                    b.Property<int?>("Variant");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("RulesetID", "Variant");
-
-                    b.ToTable("Settings");
-                });
-
-            modelBuilder.Entity("osu.Game.Input.Bindings.DatabasedKeyBinding", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<int>("IntAction")
-                        .HasColumnName("Action");
-
-                    b.Property<string>("KeysString")
-                        .HasColumnName("Keys");
-
-                    b.Property<int?>("RulesetID");
-
-                    b.Property<int?>("Variant");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("IntAction");
-
-                    b.HasIndex("RulesetID", "Variant");
-
-                    b.ToTable("KeyBinding");
-                });
-
-            modelBuilder.Entity("osu.Game.IO.FileInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<string>("Hash");
-
-                    b.Property<int>("ReferenceCount");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("Hash")
-                        .IsUnique();
-
-                    b.HasIndex("ReferenceCount");
-
-                    b.ToTable("FileInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Rulesets.RulesetInfo", b =>
-                {
-                    b.Property<int?>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<bool>("Available");
-
-                    b.Property<string>("InstantiationInfo");
-
-                    b.Property<string>("Name");
-
-                    b.Property<string>("ShortName");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("Available");
-
-                    b.HasIndex("ShortName")
-                        .IsUnique();
-
-                    b.ToTable("RulesetInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<int>("FileInfoID");
-
-                    b.Property<string>("Filename")
-                        .IsRequired();
-
-                    b.Property<int>("SkinInfoID");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("FileInfoID");
-
-                    b.HasIndex("SkinInfoID");
-
-                    b.ToTable("SkinFileInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Skinning.SkinInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<string>("Creator");
-
-                    b.Property<bool>("DeletePending");
-
-                    b.Property<string>("Name");
-
-                    b.HasKey("ID");
-
-                    b.ToTable("SkinInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b =>
-                {
-                    b.HasOne("osu.Game.Beatmaps.BeatmapDifficulty", "BaseDifficulty")
-                        .WithMany()
-                        .HasForeignKey("BaseDifficultyID")
-                        .OnDelete(DeleteBehavior.Cascade);
-
-                    b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo", "BeatmapSet")
-                        .WithMany("Beatmaps")
-                        .HasForeignKey("BeatmapSetInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-
-                    b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata")
-                        .WithMany("Beatmaps")
-                        .HasForeignKey("MetadataID");
-
-                    b.HasOne("osu.Game.Rulesets.RulesetInfo", "Ruleset")
-                        .WithMany()
-                        .HasForeignKey("RulesetID")
-                        .OnDelete(DeleteBehavior.Cascade);
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b =>
-                {
-                    b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo")
-                        .WithMany("Files")
-                        .HasForeignKey("BeatmapSetInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-
-                    b.HasOne("osu.Game.IO.FileInfo", "FileInfo")
-                        .WithMany()
-                        .HasForeignKey("FileInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b =>
-                {
-                    b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata")
-                        .WithMany("BeatmapSets")
-                        .HasForeignKey("MetadataID");
-                });
-
-            modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b =>
-                {
-                    b.HasOne("osu.Game.IO.FileInfo", "FileInfo")
-                        .WithMany()
-                        .HasForeignKey("FileInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-
-                    b.HasOne("osu.Game.Skinning.SkinInfo")
-                        .WithMany("Files")
-                        .HasForeignKey("SkinInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-                });
-#pragma warning restore 612, 618
-        }
-    }
-}
diff --git a/osu.Game/Migrations/20181007180454_StandardizePaths.cs b/osu.Game/Migrations/20181007180454_StandardizePaths.cs
deleted file mode 100644
index 274b8030a9..0000000000
--- a/osu.Game/Migrations/20181007180454_StandardizePaths.cs
+++ /dev/null
@@ -1,26 +0,0 @@
-using System;
-using Microsoft.EntityFrameworkCore.Migrations;
-using System.IO;
-
-namespace osu.Game.Migrations
-{
-    public partial class StandardizePaths : Migration
-    {
-        protected override void Up(MigrationBuilder migrationBuilder)
-        {
-            string windowsStyle = @"\";
-            string standardized = "/";
-
-            // Escaping \ does not seem to be needed.
-            migrationBuilder.Sql($"UPDATE `BeatmapInfo` SET `Path` = REPLACE(`Path`, '{windowsStyle}', '{standardized}')");
-            migrationBuilder.Sql($"UPDATE `BeatmapMetadata` SET `AudioFile` = REPLACE(`AudioFile`, '{windowsStyle}', '{standardized}')");
-            migrationBuilder.Sql($"UPDATE `BeatmapMetadata` SET `BackgroundFile` = REPLACE(`BackgroundFile`, '{windowsStyle}', '{standardized}')");
-            migrationBuilder.Sql($"UPDATE `BeatmapSetFileInfo` SET `Filename` = REPLACE(`Filename`, '{windowsStyle}', '{standardized}')");
-            migrationBuilder.Sql($"UPDATE `SkinFileInfo` SET `Filename` = REPLACE(`Filename`, '{windowsStyle}', '{standardized}')");
-        }
-
-        protected override void Down(MigrationBuilder migrationBuilder)
-        {
-        }
-    }
-}
diff --git a/osu.Game/Migrations/20181128100659_AddSkinInfoHash.Designer.cs b/osu.Game/Migrations/20181128100659_AddSkinInfoHash.Designer.cs
deleted file mode 100644
index 120674671a..0000000000
--- a/osu.Game/Migrations/20181128100659_AddSkinInfoHash.Designer.cs
+++ /dev/null
@@ -1,387 +0,0 @@
-// <auto-generated />
-using System;
-using Microsoft.EntityFrameworkCore;
-using Microsoft.EntityFrameworkCore.Infrastructure;
-using Microsoft.EntityFrameworkCore.Migrations;
-using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
-using osu.Game.Database;
-
-namespace osu.Game.Migrations
-{
-    [DbContext(typeof(OsuDbContext))]
-    [Migration("20181128100659_AddSkinInfoHash")]
-    partial class AddSkinInfoHash
-    {
-        protected override void BuildTargetModel(ModelBuilder modelBuilder)
-        {
-#pragma warning disable 612, 618
-            modelBuilder
-                .HasAnnotation("ProductVersion", "2.1.4-rtm-31024");
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapDifficulty", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<float>("ApproachRate");
-
-                    b.Property<float>("CircleSize");
-
-                    b.Property<float>("DrainRate");
-
-                    b.Property<float>("OverallDifficulty");
-
-                    b.Property<double>("SliderMultiplier");
-
-                    b.Property<double>("SliderTickRate");
-
-                    b.HasKey("ID");
-
-                    b.ToTable("BeatmapDifficulty");
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<int>("AudioLeadIn");
-
-                    b.Property<int>("BaseDifficultyID");
-
-                    b.Property<int>("BeatDivisor");
-
-                    b.Property<int>("BeatmapSetInfoID");
-
-                    b.Property<bool>("Countdown");
-
-                    b.Property<double>("DistanceSpacing");
-
-                    b.Property<int>("GridSize");
-
-                    b.Property<string>("Hash");
-
-                    b.Property<bool>("Hidden");
-
-                    b.Property<bool>("LetterboxInBreaks");
-
-                    b.Property<string>("MD5Hash");
-
-                    b.Property<int?>("MetadataID");
-
-                    b.Property<int?>("OnlineBeatmapID");
-
-                    b.Property<string>("Path");
-
-                    b.Property<int>("RulesetID");
-
-                    b.Property<bool>("SpecialStyle");
-
-                    b.Property<float>("StackLeniency");
-
-                    b.Property<double>("StarDifficulty");
-
-                    b.Property<int>("Status");
-
-                    b.Property<string>("StoredBookmarks");
-
-                    b.Property<double>("TimelineZoom");
-
-                    b.Property<string>("Version");
-
-                    b.Property<bool>("WidescreenStoryboard");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("BaseDifficultyID");
-
-                    b.HasIndex("BeatmapSetInfoID");
-
-                    b.HasIndex("Hash");
-
-                    b.HasIndex("MD5Hash");
-
-                    b.HasIndex("MetadataID");
-
-                    b.HasIndex("OnlineBeatmapID")
-                        .IsUnique();
-
-                    b.HasIndex("RulesetID");
-
-                    b.ToTable("BeatmapInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapMetadata", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<string>("Artist");
-
-                    b.Property<string>("ArtistUnicode");
-
-                    b.Property<string>("AudioFile");
-
-                    b.Property<string>("AuthorString")
-                        .HasColumnName("Author");
-
-                    b.Property<string>("BackgroundFile");
-
-                    b.Property<int>("PreviewTime");
-
-                    b.Property<string>("Source");
-
-                    b.Property<string>("Tags");
-
-                    b.Property<string>("Title");
-
-                    b.Property<string>("TitleUnicode");
-
-                    b.HasKey("ID");
-
-                    b.ToTable("BeatmapMetadata");
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<int>("BeatmapSetInfoID");
-
-                    b.Property<int>("FileInfoID");
-
-                    b.Property<string>("Filename")
-                        .IsRequired();
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("BeatmapSetInfoID");
-
-                    b.HasIndex("FileInfoID");
-
-                    b.ToTable("BeatmapSetFileInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<bool>("DeletePending");
-
-                    b.Property<string>("Hash");
-
-                    b.Property<int?>("MetadataID");
-
-                    b.Property<int?>("OnlineBeatmapSetID");
-
-                    b.Property<bool>("Protected");
-
-                    b.Property<int>("Status");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("DeletePending");
-
-                    b.HasIndex("Hash")
-                        .IsUnique();
-
-                    b.HasIndex("MetadataID");
-
-                    b.HasIndex("OnlineBeatmapSetID")
-                        .IsUnique();
-
-                    b.ToTable("BeatmapSetInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Configuration.DatabasedSetting", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<int>("IntKey")
-                        .HasColumnName("Key");
-
-                    b.Property<int?>("RulesetID");
-
-                    b.Property<string>("StringValue")
-                        .HasColumnName("Value");
-
-                    b.Property<int?>("Variant");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("RulesetID", "Variant");
-
-                    b.ToTable("Settings");
-                });
-
-            modelBuilder.Entity("osu.Game.Input.Bindings.DatabasedKeyBinding", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<int>("IntAction")
-                        .HasColumnName("Action");
-
-                    b.Property<string>("KeysString")
-                        .HasColumnName("Keys");
-
-                    b.Property<int?>("RulesetID");
-
-                    b.Property<int?>("Variant");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("IntAction");
-
-                    b.HasIndex("RulesetID", "Variant");
-
-                    b.ToTable("KeyBinding");
-                });
-
-            modelBuilder.Entity("osu.Game.IO.FileInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<string>("Hash");
-
-                    b.Property<int>("ReferenceCount");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("Hash")
-                        .IsUnique();
-
-                    b.HasIndex("ReferenceCount");
-
-                    b.ToTable("FileInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Rulesets.RulesetInfo", b =>
-                {
-                    b.Property<int?>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<bool>("Available");
-
-                    b.Property<string>("InstantiationInfo");
-
-                    b.Property<string>("Name");
-
-                    b.Property<string>("ShortName");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("Available");
-
-                    b.HasIndex("ShortName")
-                        .IsUnique();
-
-                    b.ToTable("RulesetInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<int>("FileInfoID");
-
-                    b.Property<string>("Filename")
-                        .IsRequired();
-
-                    b.Property<int>("SkinInfoID");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("FileInfoID");
-
-                    b.HasIndex("SkinInfoID");
-
-                    b.ToTable("SkinFileInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Skinning.SkinInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<string>("Creator");
-
-                    b.Property<bool>("DeletePending");
-
-                    b.Property<string>("Hash");
-
-                    b.Property<string>("Name");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("DeletePending");
-
-                    b.HasIndex("Hash")
-                        .IsUnique();
-
-                    b.ToTable("SkinInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b =>
-                {
-                    b.HasOne("osu.Game.Beatmaps.BeatmapDifficulty", "BaseDifficulty")
-                        .WithMany()
-                        .HasForeignKey("BaseDifficultyID")
-                        .OnDelete(DeleteBehavior.Cascade);
-
-                    b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo", "BeatmapSet")
-                        .WithMany("Beatmaps")
-                        .HasForeignKey("BeatmapSetInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-
-                    b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata")
-                        .WithMany("Beatmaps")
-                        .HasForeignKey("MetadataID");
-
-                    b.HasOne("osu.Game.Rulesets.RulesetInfo", "Ruleset")
-                        .WithMany()
-                        .HasForeignKey("RulesetID")
-                        .OnDelete(DeleteBehavior.Cascade);
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b =>
-                {
-                    b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo")
-                        .WithMany("Files")
-                        .HasForeignKey("BeatmapSetInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-
-                    b.HasOne("osu.Game.IO.FileInfo", "FileInfo")
-                        .WithMany()
-                        .HasForeignKey("FileInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b =>
-                {
-                    b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata")
-                        .WithMany("BeatmapSets")
-                        .HasForeignKey("MetadataID");
-                });
-
-            modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b =>
-                {
-                    b.HasOne("osu.Game.IO.FileInfo", "FileInfo")
-                        .WithMany()
-                        .HasForeignKey("FileInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-
-                    b.HasOne("osu.Game.Skinning.SkinInfo")
-                        .WithMany("Files")
-                        .HasForeignKey("SkinInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-                });
-#pragma warning restore 612, 618
-        }
-    }
-}
diff --git a/osu.Game/Migrations/20181128100659_AddSkinInfoHash.cs b/osu.Game/Migrations/20181128100659_AddSkinInfoHash.cs
deleted file mode 100644
index 860264a7dd..0000000000
--- a/osu.Game/Migrations/20181128100659_AddSkinInfoHash.cs
+++ /dev/null
@@ -1,41 +0,0 @@
-using Microsoft.EntityFrameworkCore.Migrations;
-
-namespace osu.Game.Migrations
-{
-    public partial class AddSkinInfoHash : Migration
-    {
-        protected override void Up(MigrationBuilder migrationBuilder)
-        {
-            migrationBuilder.AddColumn<string>(
-                name: "Hash",
-                table: "SkinInfo",
-                nullable: true);
-
-            migrationBuilder.CreateIndex(
-                name: "IX_SkinInfo_DeletePending",
-                table: "SkinInfo",
-                column: "DeletePending");
-
-            migrationBuilder.CreateIndex(
-                name: "IX_SkinInfo_Hash",
-                table: "SkinInfo",
-                column: "Hash",
-                unique: true);
-        }
-
-        protected override void Down(MigrationBuilder migrationBuilder)
-        {
-            migrationBuilder.DropIndex(
-                name: "IX_SkinInfo_DeletePending",
-                table: "SkinInfo");
-
-            migrationBuilder.DropIndex(
-                name: "IX_SkinInfo_Hash",
-                table: "SkinInfo");
-
-            migrationBuilder.DropColumn(
-                name: "Hash",
-                table: "SkinInfo");
-        }
-    }
-}
diff --git a/osu.Game/Migrations/20181130113755_AddScoreInfoTables.Designer.cs b/osu.Game/Migrations/20181130113755_AddScoreInfoTables.Designer.cs
deleted file mode 100644
index eee53182ce..0000000000
--- a/osu.Game/Migrations/20181130113755_AddScoreInfoTables.Designer.cs
+++ /dev/null
@@ -1,484 +0,0 @@
-// <auto-generated />
-using System;
-using Microsoft.EntityFrameworkCore;
-using Microsoft.EntityFrameworkCore.Infrastructure;
-using Microsoft.EntityFrameworkCore.Migrations;
-using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
-using osu.Game.Database;
-
-namespace osu.Game.Migrations
-{
-    [DbContext(typeof(OsuDbContext))]
-    [Migration("20181130113755_AddScoreInfoTables")]
-    partial class AddScoreInfoTables
-    {
-        protected override void BuildTargetModel(ModelBuilder modelBuilder)
-        {
-#pragma warning disable 612, 618
-            modelBuilder
-                .HasAnnotation("ProductVersion", "2.1.4-rtm-31024");
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapDifficulty", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<float>("ApproachRate");
-
-                    b.Property<float>("CircleSize");
-
-                    b.Property<float>("DrainRate");
-
-                    b.Property<float>("OverallDifficulty");
-
-                    b.Property<double>("SliderMultiplier");
-
-                    b.Property<double>("SliderTickRate");
-
-                    b.HasKey("ID");
-
-                    b.ToTable("BeatmapDifficulty");
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<int>("AudioLeadIn");
-
-                    b.Property<int>("BaseDifficultyID");
-
-                    b.Property<int>("BeatDivisor");
-
-                    b.Property<int>("BeatmapSetInfoID");
-
-                    b.Property<bool>("Countdown");
-
-                    b.Property<double>("DistanceSpacing");
-
-                    b.Property<int>("GridSize");
-
-                    b.Property<string>("Hash");
-
-                    b.Property<bool>("Hidden");
-
-                    b.Property<bool>("LetterboxInBreaks");
-
-                    b.Property<string>("MD5Hash");
-
-                    b.Property<int?>("MetadataID");
-
-                    b.Property<int?>("OnlineBeatmapID");
-
-                    b.Property<string>("Path");
-
-                    b.Property<int>("RulesetID");
-
-                    b.Property<bool>("SpecialStyle");
-
-                    b.Property<float>("StackLeniency");
-
-                    b.Property<double>("StarDifficulty");
-
-                    b.Property<int>("Status");
-
-                    b.Property<string>("StoredBookmarks");
-
-                    b.Property<double>("TimelineZoom");
-
-                    b.Property<string>("Version");
-
-                    b.Property<bool>("WidescreenStoryboard");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("BaseDifficultyID");
-
-                    b.HasIndex("BeatmapSetInfoID");
-
-                    b.HasIndex("Hash");
-
-                    b.HasIndex("MD5Hash");
-
-                    b.HasIndex("MetadataID");
-
-                    b.HasIndex("OnlineBeatmapID")
-                        .IsUnique();
-
-                    b.HasIndex("RulesetID");
-
-                    b.ToTable("BeatmapInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapMetadata", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<string>("Artist");
-
-                    b.Property<string>("ArtistUnicode");
-
-                    b.Property<string>("AudioFile");
-
-                    b.Property<string>("AuthorString")
-                        .HasColumnName("Author");
-
-                    b.Property<string>("BackgroundFile");
-
-                    b.Property<int>("PreviewTime");
-
-                    b.Property<string>("Source");
-
-                    b.Property<string>("Tags");
-
-                    b.Property<string>("Title");
-
-                    b.Property<string>("TitleUnicode");
-
-                    b.HasKey("ID");
-
-                    b.ToTable("BeatmapMetadata");
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<int>("BeatmapSetInfoID");
-
-                    b.Property<int>("FileInfoID");
-
-                    b.Property<string>("Filename")
-                        .IsRequired();
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("BeatmapSetInfoID");
-
-                    b.HasIndex("FileInfoID");
-
-                    b.ToTable("BeatmapSetFileInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<bool>("DeletePending");
-
-                    b.Property<string>("Hash");
-
-                    b.Property<int?>("MetadataID");
-
-                    b.Property<int?>("OnlineBeatmapSetID");
-
-                    b.Property<bool>("Protected");
-
-                    b.Property<int>("Status");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("DeletePending");
-
-                    b.HasIndex("Hash")
-                        .IsUnique();
-
-                    b.HasIndex("MetadataID");
-
-                    b.HasIndex("OnlineBeatmapSetID")
-                        .IsUnique();
-
-                    b.ToTable("BeatmapSetInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Configuration.DatabasedSetting", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<int>("IntKey")
-                        .HasColumnName("Key");
-
-                    b.Property<int?>("RulesetID");
-
-                    b.Property<string>("StringValue")
-                        .HasColumnName("Value");
-
-                    b.Property<int?>("Variant");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("RulesetID", "Variant");
-
-                    b.ToTable("Settings");
-                });
-
-            modelBuilder.Entity("osu.Game.Input.Bindings.DatabasedKeyBinding", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<int>("IntAction")
-                        .HasColumnName("Action");
-
-                    b.Property<string>("KeysString")
-                        .HasColumnName("Keys");
-
-                    b.Property<int?>("RulesetID");
-
-                    b.Property<int?>("Variant");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("IntAction");
-
-                    b.HasIndex("RulesetID", "Variant");
-
-                    b.ToTable("KeyBinding");
-                });
-
-            modelBuilder.Entity("osu.Game.IO.FileInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<string>("Hash");
-
-                    b.Property<int>("ReferenceCount");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("Hash")
-                        .IsUnique();
-
-                    b.HasIndex("ReferenceCount");
-
-                    b.ToTable("FileInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Rulesets.RulesetInfo", b =>
-                {
-                    b.Property<int?>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<bool>("Available");
-
-                    b.Property<string>("InstantiationInfo");
-
-                    b.Property<string>("Name");
-
-                    b.Property<string>("ShortName");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("Available");
-
-                    b.HasIndex("ShortName")
-                        .IsUnique();
-
-                    b.ToTable("RulesetInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Scoring.ScoreFileInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<int>("FileInfoID");
-
-                    b.Property<string>("Filename")
-                        .IsRequired();
-
-                    b.Property<int?>("ScoreInfoID");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("FileInfoID");
-
-                    b.HasIndex("ScoreInfoID");
-
-                    b.ToTable("ScoreFileInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Scoring.ScoreInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<double>("Accuracy")
-                        .HasColumnType("DECIMAL(1,4)");
-
-                    b.Property<int>("BeatmapInfoID");
-
-                    b.Property<int>("Combo");
-
-                    b.Property<DateTimeOffset>("Date");
-
-                    b.Property<bool>("DeletePending");
-
-                    b.Property<string>("Hash");
-
-                    b.Property<int>("MaxCombo");
-
-                    b.Property<string>("ModsJson")
-                        .HasColumnName("Mods");
-
-                    b.Property<long?>("OnlineScoreID");
-
-                    b.Property<double?>("PP");
-
-                    b.Property<int>("Rank");
-
-                    b.Property<int>("RulesetID");
-
-                    b.Property<string>("StatisticsJson")
-                        .HasColumnName("Statistics");
-
-                    b.Property<int>("TotalScore");
-
-                    b.Property<string>("UserString")
-                        .HasColumnName("User");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("BeatmapInfoID");
-
-                    b.HasIndex("OnlineScoreID")
-                        .IsUnique();
-
-                    b.HasIndex("RulesetID");
-
-                    b.ToTable("ScoreInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<int>("FileInfoID");
-
-                    b.Property<string>("Filename")
-                        .IsRequired();
-
-                    b.Property<int>("SkinInfoID");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("FileInfoID");
-
-                    b.HasIndex("SkinInfoID");
-
-                    b.ToTable("SkinFileInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Skinning.SkinInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<string>("Creator");
-
-                    b.Property<bool>("DeletePending");
-
-                    b.Property<string>("Hash");
-
-                    b.Property<string>("Name");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("DeletePending");
-
-                    b.HasIndex("Hash")
-                        .IsUnique();
-
-                    b.ToTable("SkinInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b =>
-                {
-                    b.HasOne("osu.Game.Beatmaps.BeatmapDifficulty", "BaseDifficulty")
-                        .WithMany()
-                        .HasForeignKey("BaseDifficultyID")
-                        .OnDelete(DeleteBehavior.Cascade);
-
-                    b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo", "BeatmapSet")
-                        .WithMany("Beatmaps")
-                        .HasForeignKey("BeatmapSetInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-
-                    b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata")
-                        .WithMany("Beatmaps")
-                        .HasForeignKey("MetadataID");
-
-                    b.HasOne("osu.Game.Rulesets.RulesetInfo", "Ruleset")
-                        .WithMany()
-                        .HasForeignKey("RulesetID")
-                        .OnDelete(DeleteBehavior.Cascade);
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b =>
-                {
-                    b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo")
-                        .WithMany("Files")
-                        .HasForeignKey("BeatmapSetInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-
-                    b.HasOne("osu.Game.IO.FileInfo", "FileInfo")
-                        .WithMany()
-                        .HasForeignKey("FileInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b =>
-                {
-                    b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata")
-                        .WithMany("BeatmapSets")
-                        .HasForeignKey("MetadataID");
-                });
-
-            modelBuilder.Entity("osu.Game.Scoring.ScoreFileInfo", b =>
-                {
-                    b.HasOne("osu.Game.IO.FileInfo", "FileInfo")
-                        .WithMany()
-                        .HasForeignKey("FileInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-
-                    b.HasOne("osu.Game.Scoring.ScoreInfo")
-                        .WithMany("Files")
-                        .HasForeignKey("ScoreInfoID");
-                });
-
-            modelBuilder.Entity("osu.Game.Scoring.ScoreInfo", b =>
-                {
-                    b.HasOne("osu.Game.Beatmaps.BeatmapInfo", "Beatmap")
-                        .WithMany()
-                        .HasForeignKey("BeatmapInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-
-                    b.HasOne("osu.Game.Rulesets.RulesetInfo", "Ruleset")
-                        .WithMany()
-                        .HasForeignKey("RulesetID")
-                        .OnDelete(DeleteBehavior.Cascade);
-                });
-
-            modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b =>
-                {
-                    b.HasOne("osu.Game.IO.FileInfo", "FileInfo")
-                        .WithMany()
-                        .HasForeignKey("FileInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-
-                    b.HasOne("osu.Game.Skinning.SkinInfo")
-                        .WithMany("Files")
-                        .HasForeignKey("SkinInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-                });
-#pragma warning restore 612, 618
-        }
-    }
-}
diff --git a/osu.Game/Migrations/20181130113755_AddScoreInfoTables.cs b/osu.Game/Migrations/20181130113755_AddScoreInfoTables.cs
deleted file mode 100644
index 2b6f94c5a4..0000000000
--- a/osu.Game/Migrations/20181130113755_AddScoreInfoTables.cs
+++ /dev/null
@@ -1,112 +0,0 @@
-using System;
-using Microsoft.EntityFrameworkCore.Migrations;
-
-namespace osu.Game.Migrations
-{
-    public partial class AddScoreInfoTables : Migration
-    {
-        protected override void Up(MigrationBuilder migrationBuilder)
-        {
-            migrationBuilder.CreateTable(
-                name: "ScoreInfo",
-                columns: table => new
-                {
-                    ID = table.Column<int>(nullable: false)
-                        .Annotation("Sqlite:Autoincrement", true),
-                    Rank = table.Column<int>(nullable: false),
-                    TotalScore = table.Column<int>(nullable: false),
-                    Accuracy = table.Column<double>(type: "DECIMAL(1,4)", nullable: false),
-                    PP = table.Column<double>(nullable: true),
-                    MaxCombo = table.Column<int>(nullable: false),
-                    Combo = table.Column<int>(nullable: false),
-                    RulesetID = table.Column<int>(nullable: false),
-                    Mods = table.Column<string>(nullable: true),
-                    User = table.Column<string>(nullable: true),
-                    BeatmapInfoID = table.Column<int>(nullable: false),
-                    OnlineScoreID = table.Column<long>(nullable: true),
-                    Date = table.Column<DateTimeOffset>(nullable: false),
-                    Statistics = table.Column<string>(nullable: true),
-                    Hash = table.Column<string>(nullable: true),
-                    DeletePending = table.Column<bool>(nullable: false)
-                },
-                constraints: table =>
-                {
-                    table.PrimaryKey("PK_ScoreInfo", x => x.ID);
-                    table.ForeignKey(
-                        name: "FK_ScoreInfo_BeatmapInfo_BeatmapInfoID",
-                        column: x => x.BeatmapInfoID,
-                        principalTable: "BeatmapInfo",
-                        principalColumn: "ID",
-                        onDelete: ReferentialAction.Cascade);
-                    table.ForeignKey(
-                        name: "FK_ScoreInfo_RulesetInfo_RulesetID",
-                        column: x => x.RulesetID,
-                        principalTable: "RulesetInfo",
-                        principalColumn: "ID",
-                        onDelete: ReferentialAction.Cascade);
-                });
-
-            migrationBuilder.CreateTable(
-                name: "ScoreFileInfo",
-                columns: table => new
-                {
-                    ID = table.Column<int>(nullable: false)
-                        .Annotation("Sqlite:Autoincrement", true),
-                    FileInfoID = table.Column<int>(nullable: false),
-                    Filename = table.Column<string>(nullable: false),
-                    ScoreInfoID = table.Column<int>(nullable: true)
-                },
-                constraints: table =>
-                {
-                    table.PrimaryKey("PK_ScoreFileInfo", x => x.ID);
-                    table.ForeignKey(
-                        name: "FK_ScoreFileInfo_FileInfo_FileInfoID",
-                        column: x => x.FileInfoID,
-                        principalTable: "FileInfo",
-                        principalColumn: "ID",
-                        onDelete: ReferentialAction.Cascade);
-                    table.ForeignKey(
-                        name: "FK_ScoreFileInfo_ScoreInfo_ScoreInfoID",
-                        column: x => x.ScoreInfoID,
-                        principalTable: "ScoreInfo",
-                        principalColumn: "ID",
-                        onDelete: ReferentialAction.Restrict);
-                });
-
-            migrationBuilder.CreateIndex(
-                name: "IX_ScoreFileInfo_FileInfoID",
-                table: "ScoreFileInfo",
-                column: "FileInfoID");
-
-            migrationBuilder.CreateIndex(
-                name: "IX_ScoreFileInfo_ScoreInfoID",
-                table: "ScoreFileInfo",
-                column: "ScoreInfoID");
-
-            migrationBuilder.CreateIndex(
-                name: "IX_ScoreInfo_BeatmapInfoID",
-                table: "ScoreInfo",
-                column: "BeatmapInfoID");
-
-            migrationBuilder.CreateIndex(
-                name: "IX_ScoreInfo_OnlineScoreID",
-                table: "ScoreInfo",
-                column: "OnlineScoreID",
-                unique: true);
-
-            migrationBuilder.CreateIndex(
-                name: "IX_ScoreInfo_RulesetID",
-                table: "ScoreInfo",
-                column: "RulesetID");
-        }
-
-        protected override void Down(MigrationBuilder migrationBuilder)
-        {
-            migrationBuilder.DropTable(
-                name: "ScoreFileInfo");
-
-            migrationBuilder.DropTable(
-                name: "ScoreInfo");
-        }
-    }
-}
diff --git a/osu.Game/Migrations/20190225062029_AddUserIDColumn.Designer.cs b/osu.Game/Migrations/20190225062029_AddUserIDColumn.Designer.cs
deleted file mode 100644
index 8e1e3a59f3..0000000000
--- a/osu.Game/Migrations/20190225062029_AddUserIDColumn.Designer.cs
+++ /dev/null
@@ -1,487 +0,0 @@
-// <auto-generated />
-using System;
-using Microsoft.EntityFrameworkCore;
-using Microsoft.EntityFrameworkCore.Infrastructure;
-using Microsoft.EntityFrameworkCore.Migrations;
-using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
-using osu.Game.Database;
-
-namespace osu.Game.Migrations
-{
-    [DbContext(typeof(OsuDbContext))]
-    [Migration("20190225062029_AddUserIDColumn")]
-    partial class AddUserIDColumn
-    {
-        protected override void BuildTargetModel(ModelBuilder modelBuilder)
-        {
-#pragma warning disable 612, 618
-            modelBuilder
-                .HasAnnotation("ProductVersion", "2.2.1-servicing-10028");
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapDifficulty", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<float>("ApproachRate");
-
-                    b.Property<float>("CircleSize");
-
-                    b.Property<float>("DrainRate");
-
-                    b.Property<float>("OverallDifficulty");
-
-                    b.Property<double>("SliderMultiplier");
-
-                    b.Property<double>("SliderTickRate");
-
-                    b.HasKey("ID");
-
-                    b.ToTable("BeatmapDifficulty");
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<int>("AudioLeadIn");
-
-                    b.Property<int>("BaseDifficultyID");
-
-                    b.Property<int>("BeatDivisor");
-
-                    b.Property<int>("BeatmapSetInfoID");
-
-                    b.Property<bool>("Countdown");
-
-                    b.Property<double>("DistanceSpacing");
-
-                    b.Property<int>("GridSize");
-
-                    b.Property<string>("Hash");
-
-                    b.Property<bool>("Hidden");
-
-                    b.Property<bool>("LetterboxInBreaks");
-
-                    b.Property<string>("MD5Hash");
-
-                    b.Property<int?>("MetadataID");
-
-                    b.Property<int?>("OnlineBeatmapID");
-
-                    b.Property<string>("Path");
-
-                    b.Property<int>("RulesetID");
-
-                    b.Property<bool>("SpecialStyle");
-
-                    b.Property<float>("StackLeniency");
-
-                    b.Property<double>("StarDifficulty");
-
-                    b.Property<int>("Status");
-
-                    b.Property<string>("StoredBookmarks");
-
-                    b.Property<double>("TimelineZoom");
-
-                    b.Property<string>("Version");
-
-                    b.Property<bool>("WidescreenStoryboard");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("BaseDifficultyID");
-
-                    b.HasIndex("BeatmapSetInfoID");
-
-                    b.HasIndex("Hash");
-
-                    b.HasIndex("MD5Hash");
-
-                    b.HasIndex("MetadataID");
-
-                    b.HasIndex("OnlineBeatmapID")
-                        .IsUnique();
-
-                    b.HasIndex("RulesetID");
-
-                    b.ToTable("BeatmapInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapMetadata", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<string>("Artist");
-
-                    b.Property<string>("ArtistUnicode");
-
-                    b.Property<string>("AudioFile");
-
-                    b.Property<string>("AuthorString")
-                        .HasColumnName("Author");
-
-                    b.Property<string>("BackgroundFile");
-
-                    b.Property<int>("PreviewTime");
-
-                    b.Property<string>("Source");
-
-                    b.Property<string>("Tags");
-
-                    b.Property<string>("Title");
-
-                    b.Property<string>("TitleUnicode");
-
-                    b.HasKey("ID");
-
-                    b.ToTable("BeatmapMetadata");
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<int>("BeatmapSetInfoID");
-
-                    b.Property<int>("FileInfoID");
-
-                    b.Property<string>("Filename")
-                        .IsRequired();
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("BeatmapSetInfoID");
-
-                    b.HasIndex("FileInfoID");
-
-                    b.ToTable("BeatmapSetFileInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<bool>("DeletePending");
-
-                    b.Property<string>("Hash");
-
-                    b.Property<int?>("MetadataID");
-
-                    b.Property<int?>("OnlineBeatmapSetID");
-
-                    b.Property<bool>("Protected");
-
-                    b.Property<int>("Status");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("DeletePending");
-
-                    b.HasIndex("Hash")
-                        .IsUnique();
-
-                    b.HasIndex("MetadataID");
-
-                    b.HasIndex("OnlineBeatmapSetID")
-                        .IsUnique();
-
-                    b.ToTable("BeatmapSetInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Configuration.DatabasedSetting", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<int>("IntKey")
-                        .HasColumnName("Key");
-
-                    b.Property<int?>("RulesetID");
-
-                    b.Property<string>("StringValue")
-                        .HasColumnName("Value");
-
-                    b.Property<int?>("Variant");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("RulesetID", "Variant");
-
-                    b.ToTable("Settings");
-                });
-
-            modelBuilder.Entity("osu.Game.IO.FileInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<string>("Hash");
-
-                    b.Property<int>("ReferenceCount");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("Hash")
-                        .IsUnique();
-
-                    b.HasIndex("ReferenceCount");
-
-                    b.ToTable("FileInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Input.Bindings.DatabasedKeyBinding", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<int>("IntAction")
-                        .HasColumnName("Action");
-
-                    b.Property<string>("KeysString")
-                        .HasColumnName("Keys");
-
-                    b.Property<int?>("RulesetID");
-
-                    b.Property<int?>("Variant");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("IntAction");
-
-                    b.HasIndex("RulesetID", "Variant");
-
-                    b.ToTable("KeyBinding");
-                });
-
-            modelBuilder.Entity("osu.Game.Rulesets.RulesetInfo", b =>
-                {
-                    b.Property<int?>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<bool>("Available");
-
-                    b.Property<string>("InstantiationInfo");
-
-                    b.Property<string>("Name");
-
-                    b.Property<string>("ShortName");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("Available");
-
-                    b.HasIndex("ShortName")
-                        .IsUnique();
-
-                    b.ToTable("RulesetInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Scoring.ScoreFileInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<int>("FileInfoID");
-
-                    b.Property<string>("Filename")
-                        .IsRequired();
-
-                    b.Property<int?>("ScoreInfoID");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("FileInfoID");
-
-                    b.HasIndex("ScoreInfoID");
-
-                    b.ToTable("ScoreFileInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Scoring.ScoreInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<double>("Accuracy")
-                        .HasColumnType("DECIMAL(1,4)");
-
-                    b.Property<int>("BeatmapInfoID");
-
-                    b.Property<int>("Combo");
-
-                    b.Property<DateTimeOffset>("Date");
-
-                    b.Property<bool>("DeletePending");
-
-                    b.Property<string>("Hash");
-
-                    b.Property<int>("MaxCombo");
-
-                    b.Property<string>("ModsJson")
-                        .HasColumnName("Mods");
-
-                    b.Property<long?>("OnlineScoreID");
-
-                    b.Property<double?>("PP");
-
-                    b.Property<int>("Rank");
-
-                    b.Property<int>("RulesetID");
-
-                    b.Property<string>("StatisticsJson")
-                        .HasColumnName("Statistics");
-
-                    b.Property<int>("TotalScore");
-
-                    b.Property<long?>("UserID")
-                        .HasColumnName("UserID");
-
-                    b.Property<string>("UserString")
-                        .HasColumnName("User");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("BeatmapInfoID");
-
-                    b.HasIndex("OnlineScoreID")
-                        .IsUnique();
-
-                    b.HasIndex("RulesetID");
-
-                    b.ToTable("ScoreInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<int>("FileInfoID");
-
-                    b.Property<string>("Filename")
-                        .IsRequired();
-
-                    b.Property<int>("SkinInfoID");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("FileInfoID");
-
-                    b.HasIndex("SkinInfoID");
-
-                    b.ToTable("SkinFileInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Skinning.SkinInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<string>("Creator");
-
-                    b.Property<bool>("DeletePending");
-
-                    b.Property<string>("Hash");
-
-                    b.Property<string>("Name");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("DeletePending");
-
-                    b.HasIndex("Hash")
-                        .IsUnique();
-
-                    b.ToTable("SkinInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b =>
-                {
-                    b.HasOne("osu.Game.Beatmaps.BeatmapDifficulty", "BaseDifficulty")
-                        .WithMany()
-                        .HasForeignKey("BaseDifficultyID")
-                        .OnDelete(DeleteBehavior.Cascade);
-
-                    b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo", "BeatmapSet")
-                        .WithMany("Beatmaps")
-                        .HasForeignKey("BeatmapSetInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-
-                    b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata")
-                        .WithMany("Beatmaps")
-                        .HasForeignKey("MetadataID");
-
-                    b.HasOne("osu.Game.Rulesets.RulesetInfo", "Ruleset")
-                        .WithMany()
-                        .HasForeignKey("RulesetID")
-                        .OnDelete(DeleteBehavior.Cascade);
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b =>
-                {
-                    b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo")
-                        .WithMany("Files")
-                        .HasForeignKey("BeatmapSetInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-
-                    b.HasOne("osu.Game.IO.FileInfo", "FileInfo")
-                        .WithMany()
-                        .HasForeignKey("FileInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b =>
-                {
-                    b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata")
-                        .WithMany("BeatmapSets")
-                        .HasForeignKey("MetadataID");
-                });
-
-            modelBuilder.Entity("osu.Game.Scoring.ScoreFileInfo", b =>
-                {
-                    b.HasOne("osu.Game.IO.FileInfo", "FileInfo")
-                        .WithMany()
-                        .HasForeignKey("FileInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-
-                    b.HasOne("osu.Game.Scoring.ScoreInfo")
-                        .WithMany("Files")
-                        .HasForeignKey("ScoreInfoID");
-                });
-
-            modelBuilder.Entity("osu.Game.Scoring.ScoreInfo", b =>
-                {
-                    b.HasOne("osu.Game.Beatmaps.BeatmapInfo", "Beatmap")
-                        .WithMany("Scores")
-                        .HasForeignKey("BeatmapInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-
-                    b.HasOne("osu.Game.Rulesets.RulesetInfo", "Ruleset")
-                        .WithMany()
-                        .HasForeignKey("RulesetID")
-                        .OnDelete(DeleteBehavior.Cascade);
-                });
-
-            modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b =>
-                {
-                    b.HasOne("osu.Game.IO.FileInfo", "FileInfo")
-                        .WithMany()
-                        .HasForeignKey("FileInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-
-                    b.HasOne("osu.Game.Skinning.SkinInfo")
-                        .WithMany("Files")
-                        .HasForeignKey("SkinInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-                });
-#pragma warning restore 612, 618
-        }
-    }
-}
diff --git a/osu.Game/Migrations/20190225062029_AddUserIDColumn.cs b/osu.Game/Migrations/20190225062029_AddUserIDColumn.cs
deleted file mode 100644
index 0720e0eac7..0000000000
--- a/osu.Game/Migrations/20190225062029_AddUserIDColumn.cs
+++ /dev/null
@@ -1,22 +0,0 @@
-using Microsoft.EntityFrameworkCore.Migrations;
-
-namespace osu.Game.Migrations
-{
-    public partial class AddUserIDColumn : Migration
-    {
-        protected override void Up(MigrationBuilder migrationBuilder)
-        {
-            migrationBuilder.AddColumn<long>(
-                name: "UserID",
-                table: "ScoreInfo",
-                nullable: true);
-        }
-
-        protected override void Down(MigrationBuilder migrationBuilder)
-        {
-            migrationBuilder.DropColumn(
-                name: "UserID",
-                table: "ScoreInfo");
-        }
-    }
-}
diff --git a/osu.Game/Migrations/20190525060824_SkinSettings.Designer.cs b/osu.Game/Migrations/20190525060824_SkinSettings.Designer.cs
deleted file mode 100644
index 348c42adb9..0000000000
--- a/osu.Game/Migrations/20190525060824_SkinSettings.Designer.cs
+++ /dev/null
@@ -1,498 +0,0 @@
-// <auto-generated />
-using System;
-using Microsoft.EntityFrameworkCore;
-using Microsoft.EntityFrameworkCore.Infrastructure;
-using Microsoft.EntityFrameworkCore.Migrations;
-using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
-using osu.Game.Database;
-
-namespace osu.Game.Migrations
-{
-    [DbContext(typeof(OsuDbContext))]
-    [Migration("20190525060824_SkinSettings")]
-    partial class SkinSettings
-    {
-        protected override void BuildTargetModel(ModelBuilder modelBuilder)
-        {
-#pragma warning disable 612, 618
-            modelBuilder
-                .HasAnnotation("ProductVersion", "2.2.4-servicing-10062");
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapDifficulty", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<float>("ApproachRate");
-
-                    b.Property<float>("CircleSize");
-
-                    b.Property<float>("DrainRate");
-
-                    b.Property<float>("OverallDifficulty");
-
-                    b.Property<double>("SliderMultiplier");
-
-                    b.Property<double>("SliderTickRate");
-
-                    b.HasKey("ID");
-
-                    b.ToTable("BeatmapDifficulty");
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<int>("AudioLeadIn");
-
-                    b.Property<int>("BaseDifficultyID");
-
-                    b.Property<int>("BeatDivisor");
-
-                    b.Property<int>("BeatmapSetInfoID");
-
-                    b.Property<bool>("Countdown");
-
-                    b.Property<double>("DistanceSpacing");
-
-                    b.Property<int>("GridSize");
-
-                    b.Property<string>("Hash");
-
-                    b.Property<bool>("Hidden");
-
-                    b.Property<bool>("LetterboxInBreaks");
-
-                    b.Property<string>("MD5Hash");
-
-                    b.Property<int?>("MetadataID");
-
-                    b.Property<int?>("OnlineBeatmapID");
-
-                    b.Property<string>("Path");
-
-                    b.Property<int>("RulesetID");
-
-                    b.Property<bool>("SpecialStyle");
-
-                    b.Property<float>("StackLeniency");
-
-                    b.Property<double>("StarDifficulty");
-
-                    b.Property<int>("Status");
-
-                    b.Property<string>("StoredBookmarks");
-
-                    b.Property<double>("TimelineZoom");
-
-                    b.Property<string>("Version");
-
-                    b.Property<bool>("WidescreenStoryboard");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("BaseDifficultyID");
-
-                    b.HasIndex("BeatmapSetInfoID");
-
-                    b.HasIndex("Hash");
-
-                    b.HasIndex("MD5Hash");
-
-                    b.HasIndex("MetadataID");
-
-                    b.HasIndex("OnlineBeatmapID")
-                        .IsUnique();
-
-                    b.HasIndex("RulesetID");
-
-                    b.ToTable("BeatmapInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapMetadata", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<string>("Artist");
-
-                    b.Property<string>("ArtistUnicode");
-
-                    b.Property<string>("AudioFile");
-
-                    b.Property<string>("AuthorString")
-                        .HasColumnName("Author");
-
-                    b.Property<string>("BackgroundFile");
-
-                    b.Property<int>("PreviewTime");
-
-                    b.Property<string>("Source");
-
-                    b.Property<string>("Tags");
-
-                    b.Property<string>("Title");
-
-                    b.Property<string>("TitleUnicode");
-
-                    b.HasKey("ID");
-
-                    b.ToTable("BeatmapMetadata");
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<int>("BeatmapSetInfoID");
-
-                    b.Property<int>("FileInfoID");
-
-                    b.Property<string>("Filename")
-                        .IsRequired();
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("BeatmapSetInfoID");
-
-                    b.HasIndex("FileInfoID");
-
-                    b.ToTable("BeatmapSetFileInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<bool>("DeletePending");
-
-                    b.Property<string>("Hash");
-
-                    b.Property<int?>("MetadataID");
-
-                    b.Property<int?>("OnlineBeatmapSetID");
-
-                    b.Property<bool>("Protected");
-
-                    b.Property<int>("Status");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("DeletePending");
-
-                    b.HasIndex("Hash")
-                        .IsUnique();
-
-                    b.HasIndex("MetadataID");
-
-                    b.HasIndex("OnlineBeatmapSetID")
-                        .IsUnique();
-
-                    b.ToTable("BeatmapSetInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Configuration.DatabasedSetting", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<string>("Key")
-                        .HasColumnName("Key");
-
-                    b.Property<int?>("RulesetID");
-
-                    b.Property<int?>("SkinInfoID");
-
-                    b.Property<string>("StringValue")
-                        .HasColumnName("Value");
-
-                    b.Property<int?>("Variant");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("SkinInfoID");
-
-                    b.HasIndex("RulesetID", "Variant");
-
-                    b.ToTable("Settings");
-                });
-
-            modelBuilder.Entity("osu.Game.IO.FileInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<string>("Hash");
-
-                    b.Property<int>("ReferenceCount");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("Hash")
-                        .IsUnique();
-
-                    b.HasIndex("ReferenceCount");
-
-                    b.ToTable("FileInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Input.Bindings.DatabasedKeyBinding", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<int>("IntAction")
-                        .HasColumnName("Action");
-
-                    b.Property<string>("KeysString")
-                        .HasColumnName("Keys");
-
-                    b.Property<int?>("RulesetID");
-
-                    b.Property<int?>("Variant");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("IntAction");
-
-                    b.HasIndex("RulesetID", "Variant");
-
-                    b.ToTable("KeyBinding");
-                });
-
-            modelBuilder.Entity("osu.Game.Rulesets.RulesetInfo", b =>
-                {
-                    b.Property<int?>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<bool>("Available");
-
-                    b.Property<string>("InstantiationInfo");
-
-                    b.Property<string>("Name");
-
-                    b.Property<string>("ShortName");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("Available");
-
-                    b.HasIndex("ShortName")
-                        .IsUnique();
-
-                    b.ToTable("RulesetInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Scoring.ScoreFileInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<int>("FileInfoID");
-
-                    b.Property<string>("Filename")
-                        .IsRequired();
-
-                    b.Property<int?>("ScoreInfoID");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("FileInfoID");
-
-                    b.HasIndex("ScoreInfoID");
-
-                    b.ToTable("ScoreFileInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Scoring.ScoreInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<double>("Accuracy")
-                        .HasColumnType("DECIMAL(1,4)");
-
-                    b.Property<int>("BeatmapInfoID");
-
-                    b.Property<int>("Combo");
-
-                    b.Property<DateTimeOffset>("Date");
-
-                    b.Property<bool>("DeletePending");
-
-                    b.Property<string>("Hash");
-
-                    b.Property<int>("MaxCombo");
-
-                    b.Property<string>("ModsJson")
-                        .HasColumnName("Mods");
-
-                    b.Property<long?>("OnlineScoreID");
-
-                    b.Property<double?>("PP");
-
-                    b.Property<int>("Rank");
-
-                    b.Property<int>("RulesetID");
-
-                    b.Property<string>("StatisticsJson")
-                        .HasColumnName("Statistics");
-
-                    b.Property<long>("TotalScore");
-
-                    b.Property<long?>("UserID")
-                        .HasColumnName("UserID");
-
-                    b.Property<string>("UserString")
-                        .HasColumnName("User");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("BeatmapInfoID");
-
-                    b.HasIndex("OnlineScoreID")
-                        .IsUnique();
-
-                    b.HasIndex("RulesetID");
-
-                    b.ToTable("ScoreInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<int>("FileInfoID");
-
-                    b.Property<string>("Filename")
-                        .IsRequired();
-
-                    b.Property<int>("SkinInfoID");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("FileInfoID");
-
-                    b.HasIndex("SkinInfoID");
-
-                    b.ToTable("SkinFileInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Skinning.SkinInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<string>("Creator");
-
-                    b.Property<bool>("DeletePending");
-
-                    b.Property<string>("Hash");
-
-                    b.Property<string>("Name");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("DeletePending");
-
-                    b.HasIndex("Hash")
-                        .IsUnique();
-
-                    b.ToTable("SkinInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b =>
-                {
-                    b.HasOne("osu.Game.Beatmaps.BeatmapDifficulty", "BaseDifficulty")
-                        .WithMany()
-                        .HasForeignKey("BaseDifficultyID")
-                        .OnDelete(DeleteBehavior.Cascade);
-
-                    b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo", "BeatmapSet")
-                        .WithMany("Beatmaps")
-                        .HasForeignKey("BeatmapSetInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-
-                    b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata")
-                        .WithMany("Beatmaps")
-                        .HasForeignKey("MetadataID");
-
-                    b.HasOne("osu.Game.Rulesets.RulesetInfo", "Ruleset")
-                        .WithMany()
-                        .HasForeignKey("RulesetID")
-                        .OnDelete(DeleteBehavior.Cascade);
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b =>
-                {
-                    b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo")
-                        .WithMany("Files")
-                        .HasForeignKey("BeatmapSetInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-
-                    b.HasOne("osu.Game.IO.FileInfo", "FileInfo")
-                        .WithMany()
-                        .HasForeignKey("FileInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b =>
-                {
-                    b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata")
-                        .WithMany("BeatmapSets")
-                        .HasForeignKey("MetadataID");
-                });
-
-            modelBuilder.Entity("osu.Game.Configuration.DatabasedSetting", b =>
-                {
-                    b.HasOne("osu.Game.Skinning.SkinInfo")
-                        .WithMany("Settings")
-                        .HasForeignKey("SkinInfoID");
-                });
-
-            modelBuilder.Entity("osu.Game.Scoring.ScoreFileInfo", b =>
-                {
-                    b.HasOne("osu.Game.IO.FileInfo", "FileInfo")
-                        .WithMany()
-                        .HasForeignKey("FileInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-
-                    b.HasOne("osu.Game.Scoring.ScoreInfo")
-                        .WithMany("Files")
-                        .HasForeignKey("ScoreInfoID");
-                });
-
-            modelBuilder.Entity("osu.Game.Scoring.ScoreInfo", b =>
-                {
-                    b.HasOne("osu.Game.Beatmaps.BeatmapInfo", "Beatmap")
-                        .WithMany("Scores")
-                        .HasForeignKey("BeatmapInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-
-                    b.HasOne("osu.Game.Rulesets.RulesetInfo", "Ruleset")
-                        .WithMany()
-                        .HasForeignKey("RulesetID")
-                        .OnDelete(DeleteBehavior.Cascade);
-                });
-
-            modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b =>
-                {
-                    b.HasOne("osu.Game.IO.FileInfo", "FileInfo")
-                        .WithMany()
-                        .HasForeignKey("FileInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-
-                    b.HasOne("osu.Game.Skinning.SkinInfo")
-                        .WithMany("Files")
-                        .HasForeignKey("SkinInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-                });
-#pragma warning restore 612, 618
-        }
-    }
-}
diff --git a/osu.Game/Migrations/20190525060824_SkinSettings.cs b/osu.Game/Migrations/20190525060824_SkinSettings.cs
deleted file mode 100644
index 99237419b7..0000000000
--- a/osu.Game/Migrations/20190525060824_SkinSettings.cs
+++ /dev/null
@@ -1,54 +0,0 @@
-using Microsoft.EntityFrameworkCore.Migrations;
-
-namespace osu.Game.Migrations
-{
-    public partial class SkinSettings : Migration
-    {
-        protected override void Up(MigrationBuilder migrationBuilder)
-        {
-            migrationBuilder.Sql(@"create table Settings_dg_tmp
-            (
-	            ID INTEGER not null
-		            constraint PK_Settings
-			            primary key autoincrement,
-	            Key TEXT not null,
-	            RulesetID INTEGER,
-	            Value TEXT,
-	            Variant INTEGER,
-	            SkinInfoID int
-		            constraint Settings_SkinInfo_ID_fk
-			            references SkinInfo
-				            on delete restrict
-            );
-
-            insert into Settings_dg_tmp(ID, Key, RulesetID, Value, Variant) select ID, Key, RulesetID, Value, Variant from Settings;
-
-            drop table Settings;
-
-            alter table Settings_dg_tmp rename to Settings;
-
-            create index IX_Settings_RulesetID_Variant
-	            on Settings (RulesetID, Variant);
-
-            create index Settings_SkinInfoID_index
-	            on Settings (SkinInfoID);
-
-            ");
-        }
-
-        protected override void Down(MigrationBuilder migrationBuilder)
-        {
-            migrationBuilder.DropForeignKey(
-                name: "FK_Settings_SkinInfo_SkinInfoID",
-                table: "Settings");
-
-            migrationBuilder.DropIndex(
-                name: "IX_Settings_SkinInfoID",
-                table: "Settings");
-
-            migrationBuilder.DropColumn(
-                name: "SkinInfoID",
-                table: "Settings");
-        }
-    }
-}
diff --git a/osu.Game/Migrations/20190605091246_AddDateAddedColumnToBeatmapSet.Designer.cs b/osu.Game/Migrations/20190605091246_AddDateAddedColumnToBeatmapSet.Designer.cs
deleted file mode 100644
index 9477369aa0..0000000000
--- a/osu.Game/Migrations/20190605091246_AddDateAddedColumnToBeatmapSet.Designer.cs
+++ /dev/null
@@ -1,489 +0,0 @@
-// <auto-generated />
-using System;
-using Microsoft.EntityFrameworkCore;
-using Microsoft.EntityFrameworkCore.Infrastructure;
-using Microsoft.EntityFrameworkCore.Migrations;
-using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
-using osu.Game.Database;
-
-namespace osu.Game.Migrations
-{
-    [DbContext(typeof(OsuDbContext))]
-    [Migration("20190605091246_AddDateAddedColumnToBeatmapSet")]
-    partial class AddDateAddedColumnToBeatmapSet
-    {
-        protected override void BuildTargetModel(ModelBuilder modelBuilder)
-        {
-#pragma warning disable 612, 618
-            modelBuilder
-                .HasAnnotation("ProductVersion", "2.2.4-servicing-10062");
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapDifficulty", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<float>("ApproachRate");
-
-                    b.Property<float>("CircleSize");
-
-                    b.Property<float>("DrainRate");
-
-                    b.Property<float>("OverallDifficulty");
-
-                    b.Property<double>("SliderMultiplier");
-
-                    b.Property<double>("SliderTickRate");
-
-                    b.HasKey("ID");
-
-                    b.ToTable("BeatmapDifficulty");
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<int>("AudioLeadIn");
-
-                    b.Property<int>("BaseDifficultyID");
-
-                    b.Property<int>("BeatDivisor");
-
-                    b.Property<int>("BeatmapSetInfoID");
-
-                    b.Property<bool>("Countdown");
-
-                    b.Property<double>("DistanceSpacing");
-
-                    b.Property<int>("GridSize");
-
-                    b.Property<string>("Hash");
-
-                    b.Property<bool>("Hidden");
-
-                    b.Property<bool>("LetterboxInBreaks");
-
-                    b.Property<string>("MD5Hash");
-
-                    b.Property<int?>("MetadataID");
-
-                    b.Property<int?>("OnlineBeatmapID");
-
-                    b.Property<string>("Path");
-
-                    b.Property<int>("RulesetID");
-
-                    b.Property<bool>("SpecialStyle");
-
-                    b.Property<float>("StackLeniency");
-
-                    b.Property<double>("StarDifficulty");
-
-                    b.Property<int>("Status");
-
-                    b.Property<string>("StoredBookmarks");
-
-                    b.Property<double>("TimelineZoom");
-
-                    b.Property<string>("Version");
-
-                    b.Property<bool>("WidescreenStoryboard");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("BaseDifficultyID");
-
-                    b.HasIndex("BeatmapSetInfoID");
-
-                    b.HasIndex("Hash");
-
-                    b.HasIndex("MD5Hash");
-
-                    b.HasIndex("MetadataID");
-
-                    b.HasIndex("OnlineBeatmapID")
-                        .IsUnique();
-
-                    b.HasIndex("RulesetID");
-
-                    b.ToTable("BeatmapInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapMetadata", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<string>("Artist");
-
-                    b.Property<string>("ArtistUnicode");
-
-                    b.Property<string>("AudioFile");
-
-                    b.Property<string>("AuthorString")
-                        .HasColumnName("Author");
-
-                    b.Property<string>("BackgroundFile");
-
-                    b.Property<int>("PreviewTime");
-
-                    b.Property<string>("Source");
-
-                    b.Property<string>("Tags");
-
-                    b.Property<string>("Title");
-
-                    b.Property<string>("TitleUnicode");
-
-                    b.HasKey("ID");
-
-                    b.ToTable("BeatmapMetadata");
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<int>("BeatmapSetInfoID");
-
-                    b.Property<int>("FileInfoID");
-
-                    b.Property<string>("Filename")
-                        .IsRequired();
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("BeatmapSetInfoID");
-
-                    b.HasIndex("FileInfoID");
-
-                    b.ToTable("BeatmapSetFileInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<DateTimeOffset>("DateAdded");
-
-                    b.Property<bool>("DeletePending");
-
-                    b.Property<string>("Hash");
-
-                    b.Property<int?>("MetadataID");
-
-                    b.Property<int?>("OnlineBeatmapSetID");
-
-                    b.Property<bool>("Protected");
-
-                    b.Property<int>("Status");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("DeletePending");
-
-                    b.HasIndex("Hash")
-                        .IsUnique();
-
-                    b.HasIndex("MetadataID");
-
-                    b.HasIndex("OnlineBeatmapSetID")
-                        .IsUnique();
-
-                    b.ToTable("BeatmapSetInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Configuration.DatabasedSetting", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<string>("Key")
-                        .HasColumnName("Key");
-
-                    b.Property<int?>("RulesetID");
-
-                    b.Property<string>("StringValue")
-                        .HasColumnName("Value");
-
-                    b.Property<int?>("Variant");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("RulesetID", "Variant");
-
-                    b.ToTable("Settings");
-                });
-
-            modelBuilder.Entity("osu.Game.IO.FileInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<string>("Hash");
-
-                    b.Property<int>("ReferenceCount");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("Hash")
-                        .IsUnique();
-
-                    b.HasIndex("ReferenceCount");
-
-                    b.ToTable("FileInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Input.Bindings.DatabasedKeyBinding", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<int>("IntAction")
-                        .HasColumnName("Action");
-
-                    b.Property<string>("KeysString")
-                        .HasColumnName("Keys");
-
-                    b.Property<int?>("RulesetID");
-
-                    b.Property<int?>("Variant");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("IntAction");
-
-                    b.HasIndex("RulesetID", "Variant");
-
-                    b.ToTable("KeyBinding");
-                });
-
-            modelBuilder.Entity("osu.Game.Rulesets.RulesetInfo", b =>
-                {
-                    b.Property<int?>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<bool>("Available");
-
-                    b.Property<string>("InstantiationInfo");
-
-                    b.Property<string>("Name");
-
-                    b.Property<string>("ShortName");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("Available");
-
-                    b.HasIndex("ShortName")
-                        .IsUnique();
-
-                    b.ToTable("RulesetInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Scoring.ScoreFileInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<int>("FileInfoID");
-
-                    b.Property<string>("Filename")
-                        .IsRequired();
-
-                    b.Property<int?>("ScoreInfoID");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("FileInfoID");
-
-                    b.HasIndex("ScoreInfoID");
-
-                    b.ToTable("ScoreFileInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Scoring.ScoreInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<double>("Accuracy")
-                        .HasColumnType("DECIMAL(1,4)");
-
-                    b.Property<int>("BeatmapInfoID");
-
-                    b.Property<int>("Combo");
-
-                    b.Property<DateTimeOffset>("Date");
-
-                    b.Property<bool>("DeletePending");
-
-                    b.Property<string>("Hash");
-
-                    b.Property<int>("MaxCombo");
-
-                    b.Property<string>("ModsJson")
-                        .HasColumnName("Mods");
-
-                    b.Property<long?>("OnlineScoreID");
-
-                    b.Property<double?>("PP");
-
-                    b.Property<int>("Rank");
-
-                    b.Property<int>("RulesetID");
-
-                    b.Property<string>("StatisticsJson")
-                        .HasColumnName("Statistics");
-
-                    b.Property<long>("TotalScore");
-
-                    b.Property<long?>("UserID")
-                        .HasColumnName("UserID");
-
-                    b.Property<string>("UserString")
-                        .HasColumnName("User");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("BeatmapInfoID");
-
-                    b.HasIndex("OnlineScoreID")
-                        .IsUnique();
-
-                    b.HasIndex("RulesetID");
-
-                    b.ToTable("ScoreInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<int>("FileInfoID");
-
-                    b.Property<string>("Filename")
-                        .IsRequired();
-
-                    b.Property<int>("SkinInfoID");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("FileInfoID");
-
-                    b.HasIndex("SkinInfoID");
-
-                    b.ToTable("SkinFileInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Skinning.SkinInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<string>("Creator");
-
-                    b.Property<bool>("DeletePending");
-
-                    b.Property<string>("Hash");
-
-                    b.Property<string>("Name");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("DeletePending");
-
-                    b.HasIndex("Hash")
-                        .IsUnique();
-
-                    b.ToTable("SkinInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b =>
-                {
-                    b.HasOne("osu.Game.Beatmaps.BeatmapDifficulty", "BaseDifficulty")
-                        .WithMany()
-                        .HasForeignKey("BaseDifficultyID")
-                        .OnDelete(DeleteBehavior.Cascade);
-
-                    b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo", "BeatmapSet")
-                        .WithMany("Beatmaps")
-                        .HasForeignKey("BeatmapSetInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-
-                    b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata")
-                        .WithMany("Beatmaps")
-                        .HasForeignKey("MetadataID");
-
-                    b.HasOne("osu.Game.Rulesets.RulesetInfo", "Ruleset")
-                        .WithMany()
-                        .HasForeignKey("RulesetID")
-                        .OnDelete(DeleteBehavior.Cascade);
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b =>
-                {
-                    b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo")
-                        .WithMany("Files")
-                        .HasForeignKey("BeatmapSetInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-
-                    b.HasOne("osu.Game.IO.FileInfo", "FileInfo")
-                        .WithMany()
-                        .HasForeignKey("FileInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b =>
-                {
-                    b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata")
-                        .WithMany("BeatmapSets")
-                        .HasForeignKey("MetadataID");
-                });
-
-            modelBuilder.Entity("osu.Game.Scoring.ScoreFileInfo", b =>
-                {
-                    b.HasOne("osu.Game.IO.FileInfo", "FileInfo")
-                        .WithMany()
-                        .HasForeignKey("FileInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-
-                    b.HasOne("osu.Game.Scoring.ScoreInfo")
-                        .WithMany("Files")
-                        .HasForeignKey("ScoreInfoID");
-                });
-
-            modelBuilder.Entity("osu.Game.Scoring.ScoreInfo", b =>
-                {
-                    b.HasOne("osu.Game.Beatmaps.BeatmapInfo", "Beatmap")
-                        .WithMany("Scores")
-                        .HasForeignKey("BeatmapInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-
-                    b.HasOne("osu.Game.Rulesets.RulesetInfo", "Ruleset")
-                        .WithMany()
-                        .HasForeignKey("RulesetID")
-                        .OnDelete(DeleteBehavior.Cascade);
-                });
-
-            modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b =>
-                {
-                    b.HasOne("osu.Game.IO.FileInfo", "FileInfo")
-                        .WithMany()
-                        .HasForeignKey("FileInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-
-                    b.HasOne("osu.Game.Skinning.SkinInfo")
-                        .WithMany("Files")
-                        .HasForeignKey("SkinInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-                });
-#pragma warning restore 612, 618
-        }
-    }
-}
diff --git a/osu.Game/Migrations/20190605091246_AddDateAddedColumnToBeatmapSet.cs b/osu.Game/Migrations/20190605091246_AddDateAddedColumnToBeatmapSet.cs
deleted file mode 100644
index 55dc18b6a3..0000000000
--- a/osu.Game/Migrations/20190605091246_AddDateAddedColumnToBeatmapSet.cs
+++ /dev/null
@@ -1,24 +0,0 @@
-using System;
-using Microsoft.EntityFrameworkCore.Migrations;
-
-namespace osu.Game.Migrations
-{
-    public partial class AddDateAddedColumnToBeatmapSet : Migration
-    {
-        protected override void Up(MigrationBuilder migrationBuilder)
-        {
-            migrationBuilder.AddColumn<DateTimeOffset>(
-                name: "DateAdded",
-                table: "BeatmapSetInfo",
-                nullable: false,
-                defaultValue: new DateTimeOffset(new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)));
-        }
-
-        protected override void Down(MigrationBuilder migrationBuilder)
-        {
-            migrationBuilder.DropColumn(
-                name: "DateAdded",
-                table: "BeatmapSetInfo");
-        }
-    }
-}
diff --git a/osu.Game/Migrations/20190708070844_AddBPMAndLengthColumns.Designer.cs b/osu.Game/Migrations/20190708070844_AddBPMAndLengthColumns.Designer.cs
deleted file mode 100644
index c5fcc16f84..0000000000
--- a/osu.Game/Migrations/20190708070844_AddBPMAndLengthColumns.Designer.cs
+++ /dev/null
@@ -1,504 +0,0 @@
-// <auto-generated />
-using System;
-using Microsoft.EntityFrameworkCore;
-using Microsoft.EntityFrameworkCore.Infrastructure;
-using Microsoft.EntityFrameworkCore.Migrations;
-using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
-using osu.Game.Database;
-
-namespace osu.Game.Migrations
-{
-    [DbContext(typeof(OsuDbContext))]
-    [Migration("20190708070844_AddBPMAndLengthColumns")]
-    partial class AddBPMAndLengthColumns
-    {
-        protected override void BuildTargetModel(ModelBuilder modelBuilder)
-        {
-#pragma warning disable 612, 618
-            modelBuilder
-                .HasAnnotation("ProductVersion", "2.2.4-servicing-10062");
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapDifficulty", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<float>("ApproachRate");
-
-                    b.Property<float>("CircleSize");
-
-                    b.Property<float>("DrainRate");
-
-                    b.Property<float>("OverallDifficulty");
-
-                    b.Property<double>("SliderMultiplier");
-
-                    b.Property<double>("SliderTickRate");
-
-                    b.HasKey("ID");
-
-                    b.ToTable("BeatmapDifficulty");
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<int>("AudioLeadIn");
-
-                    b.Property<double>("BPM");
-
-                    b.Property<int>("BaseDifficultyID");
-
-                    b.Property<int>("BeatDivisor");
-
-                    b.Property<int>("BeatmapSetInfoID");
-
-                    b.Property<bool>("Countdown");
-
-                    b.Property<double>("DistanceSpacing");
-
-                    b.Property<int>("GridSize");
-
-                    b.Property<string>("Hash");
-
-                    b.Property<bool>("Hidden");
-
-                    b.Property<double>("Length");
-
-                    b.Property<bool>("LetterboxInBreaks");
-
-                    b.Property<string>("MD5Hash");
-
-                    b.Property<int?>("MetadataID");
-
-                    b.Property<int?>("OnlineBeatmapID");
-
-                    b.Property<string>("Path");
-
-                    b.Property<int>("RulesetID");
-
-                    b.Property<bool>("SpecialStyle");
-
-                    b.Property<float>("StackLeniency");
-
-                    b.Property<double>("StarDifficulty");
-
-                    b.Property<int>("Status");
-
-                    b.Property<string>("StoredBookmarks");
-
-                    b.Property<double>("TimelineZoom");
-
-                    b.Property<string>("Version");
-
-                    b.Property<bool>("WidescreenStoryboard");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("BaseDifficultyID");
-
-                    b.HasIndex("BeatmapSetInfoID");
-
-                    b.HasIndex("Hash");
-
-                    b.HasIndex("MD5Hash");
-
-                    b.HasIndex("MetadataID");
-
-                    b.HasIndex("OnlineBeatmapID")
-                        .IsUnique();
-
-                    b.HasIndex("RulesetID");
-
-                    b.ToTable("BeatmapInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapMetadata", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<string>("Artist");
-
-                    b.Property<string>("ArtistUnicode");
-
-                    b.Property<string>("AudioFile");
-
-                    b.Property<string>("AuthorString")
-                        .HasColumnName("Author");
-
-                    b.Property<string>("BackgroundFile");
-
-                    b.Property<int>("PreviewTime");
-
-                    b.Property<string>("Source");
-
-                    b.Property<string>("Tags");
-
-                    b.Property<string>("Title");
-
-                    b.Property<string>("TitleUnicode");
-
-                    b.HasKey("ID");
-
-                    b.ToTable("BeatmapMetadata");
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<int>("BeatmapSetInfoID");
-
-                    b.Property<int>("FileInfoID");
-
-                    b.Property<string>("Filename")
-                        .IsRequired();
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("BeatmapSetInfoID");
-
-                    b.HasIndex("FileInfoID");
-
-                    b.ToTable("BeatmapSetFileInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<DateTimeOffset>("DateAdded");
-
-                    b.Property<bool>("DeletePending");
-
-                    b.Property<string>("Hash");
-
-                    b.Property<int?>("MetadataID");
-
-                    b.Property<int?>("OnlineBeatmapSetID");
-
-                    b.Property<bool>("Protected");
-
-                    b.Property<int>("Status");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("DeletePending");
-
-                    b.HasIndex("Hash")
-                        .IsUnique();
-
-                    b.HasIndex("MetadataID");
-
-                    b.HasIndex("OnlineBeatmapSetID")
-                        .IsUnique();
-
-                    b.ToTable("BeatmapSetInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Configuration.DatabasedSetting", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<string>("Key")
-                        .HasColumnName("Key");
-
-                    b.Property<int?>("RulesetID");
-
-                    b.Property<int?>("SkinInfoID");
-
-                    b.Property<string>("StringValue")
-                        .HasColumnName("Value");
-
-                    b.Property<int?>("Variant");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("SkinInfoID");
-
-                    b.HasIndex("RulesetID", "Variant");
-
-                    b.ToTable("Settings");
-                });
-
-            modelBuilder.Entity("osu.Game.IO.FileInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<string>("Hash");
-
-                    b.Property<int>("ReferenceCount");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("Hash")
-                        .IsUnique();
-
-                    b.HasIndex("ReferenceCount");
-
-                    b.ToTable("FileInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Input.Bindings.DatabasedKeyBinding", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<int>("IntAction")
-                        .HasColumnName("Action");
-
-                    b.Property<string>("KeysString")
-                        .HasColumnName("Keys");
-
-                    b.Property<int?>("RulesetID");
-
-                    b.Property<int?>("Variant");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("IntAction");
-
-                    b.HasIndex("RulesetID", "Variant");
-
-                    b.ToTable("KeyBinding");
-                });
-
-            modelBuilder.Entity("osu.Game.Rulesets.RulesetInfo", b =>
-                {
-                    b.Property<int?>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<bool>("Available");
-
-                    b.Property<string>("InstantiationInfo");
-
-                    b.Property<string>("Name");
-
-                    b.Property<string>("ShortName");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("Available");
-
-                    b.HasIndex("ShortName")
-                        .IsUnique();
-
-                    b.ToTable("RulesetInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Scoring.ScoreFileInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<int>("FileInfoID");
-
-                    b.Property<string>("Filename")
-                        .IsRequired();
-
-                    b.Property<int?>("ScoreInfoID");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("FileInfoID");
-
-                    b.HasIndex("ScoreInfoID");
-
-                    b.ToTable("ScoreFileInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Scoring.ScoreInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<double>("Accuracy")
-                        .HasColumnType("DECIMAL(1,4)");
-
-                    b.Property<int>("BeatmapInfoID");
-
-                    b.Property<int>("Combo");
-
-                    b.Property<DateTimeOffset>("Date");
-
-                    b.Property<bool>("DeletePending");
-
-                    b.Property<string>("Hash");
-
-                    b.Property<int>("MaxCombo");
-
-                    b.Property<string>("ModsJson")
-                        .HasColumnName("Mods");
-
-                    b.Property<long?>("OnlineScoreID");
-
-                    b.Property<double?>("PP");
-
-                    b.Property<int>("Rank");
-
-                    b.Property<int>("RulesetID");
-
-                    b.Property<string>("StatisticsJson")
-                        .HasColumnName("Statistics");
-
-                    b.Property<long>("TotalScore");
-
-                    b.Property<long?>("UserID")
-                        .HasColumnName("UserID");
-
-                    b.Property<string>("UserString")
-                        .HasColumnName("User");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("BeatmapInfoID");
-
-                    b.HasIndex("OnlineScoreID")
-                        .IsUnique();
-
-                    b.HasIndex("RulesetID");
-
-                    b.ToTable("ScoreInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<int>("FileInfoID");
-
-                    b.Property<string>("Filename")
-                        .IsRequired();
-
-                    b.Property<int>("SkinInfoID");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("FileInfoID");
-
-                    b.HasIndex("SkinInfoID");
-
-                    b.ToTable("SkinFileInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Skinning.SkinInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<string>("Creator");
-
-                    b.Property<bool>("DeletePending");
-
-                    b.Property<string>("Hash");
-
-                    b.Property<string>("Name");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("DeletePending");
-
-                    b.HasIndex("Hash")
-                        .IsUnique();
-
-                    b.ToTable("SkinInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b =>
-                {
-                    b.HasOne("osu.Game.Beatmaps.BeatmapDifficulty", "BaseDifficulty")
-                        .WithMany()
-                        .HasForeignKey("BaseDifficultyID")
-                        .OnDelete(DeleteBehavior.Cascade);
-
-                    b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo", "BeatmapSet")
-                        .WithMany("Beatmaps")
-                        .HasForeignKey("BeatmapSetInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-
-                    b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata")
-                        .WithMany("Beatmaps")
-                        .HasForeignKey("MetadataID");
-
-                    b.HasOne("osu.Game.Rulesets.RulesetInfo", "Ruleset")
-                        .WithMany()
-                        .HasForeignKey("RulesetID")
-                        .OnDelete(DeleteBehavior.Cascade);
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b =>
-                {
-                    b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo")
-                        .WithMany("Files")
-                        .HasForeignKey("BeatmapSetInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-
-                    b.HasOne("osu.Game.IO.FileInfo", "FileInfo")
-                        .WithMany()
-                        .HasForeignKey("FileInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b =>
-                {
-                    b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata")
-                        .WithMany("BeatmapSets")
-                        .HasForeignKey("MetadataID");
-                });
-
-            modelBuilder.Entity("osu.Game.Configuration.DatabasedSetting", b =>
-                {
-                    b.HasOne("osu.Game.Skinning.SkinInfo")
-                        .WithMany("Settings")
-                        .HasForeignKey("SkinInfoID");
-                });
-
-            modelBuilder.Entity("osu.Game.Scoring.ScoreFileInfo", b =>
-                {
-                    b.HasOne("osu.Game.IO.FileInfo", "FileInfo")
-                        .WithMany()
-                        .HasForeignKey("FileInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-
-                    b.HasOne("osu.Game.Scoring.ScoreInfo")
-                        .WithMany("Files")
-                        .HasForeignKey("ScoreInfoID");
-                });
-
-            modelBuilder.Entity("osu.Game.Scoring.ScoreInfo", b =>
-                {
-                    b.HasOne("osu.Game.Beatmaps.BeatmapInfo", "Beatmap")
-                        .WithMany("Scores")
-                        .HasForeignKey("BeatmapInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-
-                    b.HasOne("osu.Game.Rulesets.RulesetInfo", "Ruleset")
-                        .WithMany()
-                        .HasForeignKey("RulesetID")
-                        .OnDelete(DeleteBehavior.Cascade);
-                });
-
-            modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b =>
-                {
-                    b.HasOne("osu.Game.IO.FileInfo", "FileInfo")
-                        .WithMany()
-                        .HasForeignKey("FileInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-
-                    b.HasOne("osu.Game.Skinning.SkinInfo")
-                        .WithMany("Files")
-                        .HasForeignKey("SkinInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-                });
-#pragma warning restore 612, 618
-        }
-    }
-}
diff --git a/osu.Game/Migrations/20190708070844_AddBPMAndLengthColumns.cs b/osu.Game/Migrations/20190708070844_AddBPMAndLengthColumns.cs
deleted file mode 100644
index f5963ebf5e..0000000000
--- a/osu.Game/Migrations/20190708070844_AddBPMAndLengthColumns.cs
+++ /dev/null
@@ -1,33 +0,0 @@
-using Microsoft.EntityFrameworkCore.Migrations;
-
-namespace osu.Game.Migrations
-{
-    public partial class AddBPMAndLengthColumns : Migration
-    {
-        protected override void Up(MigrationBuilder migrationBuilder)
-        {
-            migrationBuilder.AddColumn<double>(
-                name: "BPM",
-                table: "BeatmapInfo",
-                nullable: false,
-                defaultValue: 0.0);
-
-            migrationBuilder.AddColumn<double>(
-                name: "Length",
-                table: "BeatmapInfo",
-                nullable: false,
-                defaultValue: 0.0);
-        }
-
-        protected override void Down(MigrationBuilder migrationBuilder)
-        {
-            migrationBuilder.DropColumn(
-                name: "BPM",
-                table: "BeatmapInfo");
-
-            migrationBuilder.DropColumn(
-                name: "Length",
-                table: "BeatmapInfo");
-        }
-    }
-}
diff --git a/osu.Game/Migrations/20190913104727_AddBeatmapVideo.Designer.cs b/osu.Game/Migrations/20190913104727_AddBeatmapVideo.Designer.cs
deleted file mode 100644
index 826233a2b0..0000000000
--- a/osu.Game/Migrations/20190913104727_AddBeatmapVideo.Designer.cs
+++ /dev/null
@@ -1,506 +0,0 @@
-// <auto-generated />
-using System;
-using Microsoft.EntityFrameworkCore;
-using Microsoft.EntityFrameworkCore.Infrastructure;
-using Microsoft.EntityFrameworkCore.Migrations;
-using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
-using osu.Game.Database;
-
-namespace osu.Game.Migrations
-{
-    [DbContext(typeof(OsuDbContext))]
-    [Migration("20190913104727_AddBeatmapVideo")]
-    partial class AddBeatmapVideo
-    {
-        protected override void BuildTargetModel(ModelBuilder modelBuilder)
-        {
-#pragma warning disable 612, 618
-            modelBuilder
-                .HasAnnotation("ProductVersion", "2.2.6-servicing-10079");
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapDifficulty", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<float>("ApproachRate");
-
-                    b.Property<float>("CircleSize");
-
-                    b.Property<float>("DrainRate");
-
-                    b.Property<float>("OverallDifficulty");
-
-                    b.Property<double>("SliderMultiplier");
-
-                    b.Property<double>("SliderTickRate");
-
-                    b.HasKey("ID");
-
-                    b.ToTable("BeatmapDifficulty");
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<int>("AudioLeadIn");
-
-                    b.Property<double>("BPM");
-
-                    b.Property<int>("BaseDifficultyID");
-
-                    b.Property<int>("BeatDivisor");
-
-                    b.Property<int>("BeatmapSetInfoID");
-
-                    b.Property<bool>("Countdown");
-
-                    b.Property<double>("DistanceSpacing");
-
-                    b.Property<int>("GridSize");
-
-                    b.Property<string>("Hash");
-
-                    b.Property<bool>("Hidden");
-
-                    b.Property<double>("Length");
-
-                    b.Property<bool>("LetterboxInBreaks");
-
-                    b.Property<string>("MD5Hash");
-
-                    b.Property<int?>("MetadataID");
-
-                    b.Property<int?>("OnlineBeatmapID");
-
-                    b.Property<string>("Path");
-
-                    b.Property<int>("RulesetID");
-
-                    b.Property<bool>("SpecialStyle");
-
-                    b.Property<float>("StackLeniency");
-
-                    b.Property<double>("StarDifficulty");
-
-                    b.Property<int>("Status");
-
-                    b.Property<string>("StoredBookmarks");
-
-                    b.Property<double>("TimelineZoom");
-
-                    b.Property<string>("Version");
-
-                    b.Property<bool>("WidescreenStoryboard");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("BaseDifficultyID");
-
-                    b.HasIndex("BeatmapSetInfoID");
-
-                    b.HasIndex("Hash");
-
-                    b.HasIndex("MD5Hash");
-
-                    b.HasIndex("MetadataID");
-
-                    b.HasIndex("OnlineBeatmapID")
-                        .IsUnique();
-
-                    b.HasIndex("RulesetID");
-
-                    b.ToTable("BeatmapInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapMetadata", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<string>("Artist");
-
-                    b.Property<string>("ArtistUnicode");
-
-                    b.Property<string>("AudioFile");
-
-                    b.Property<string>("AuthorString")
-                        .HasColumnName("Author");
-
-                    b.Property<string>("BackgroundFile");
-
-                    b.Property<int>("PreviewTime");
-
-                    b.Property<string>("Source");
-
-                    b.Property<string>("Tags");
-
-                    b.Property<string>("Title");
-
-                    b.Property<string>("TitleUnicode");
-
-                    b.Property<string>("VideoFile");
-
-                    b.HasKey("ID");
-
-                    b.ToTable("BeatmapMetadata");
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<int>("BeatmapSetInfoID");
-
-                    b.Property<int>("FileInfoID");
-
-                    b.Property<string>("Filename")
-                        .IsRequired();
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("BeatmapSetInfoID");
-
-                    b.HasIndex("FileInfoID");
-
-                    b.ToTable("BeatmapSetFileInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<DateTimeOffset>("DateAdded");
-
-                    b.Property<bool>("DeletePending");
-
-                    b.Property<string>("Hash");
-
-                    b.Property<int?>("MetadataID");
-
-                    b.Property<int?>("OnlineBeatmapSetID");
-
-                    b.Property<bool>("Protected");
-
-                    b.Property<int>("Status");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("DeletePending");
-
-                    b.HasIndex("Hash")
-                        .IsUnique();
-
-                    b.HasIndex("MetadataID");
-
-                    b.HasIndex("OnlineBeatmapSetID")
-                        .IsUnique();
-
-                    b.ToTable("BeatmapSetInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Configuration.DatabasedSetting", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<string>("Key")
-                        .HasColumnName("Key");
-
-                    b.Property<int?>("RulesetID");
-
-                    b.Property<int?>("SkinInfoID");
-
-                    b.Property<string>("StringValue")
-                        .HasColumnName("Value");
-
-                    b.Property<int?>("Variant");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("SkinInfoID");
-
-                    b.HasIndex("RulesetID", "Variant");
-
-                    b.ToTable("Settings");
-                });
-
-            modelBuilder.Entity("osu.Game.IO.FileInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<string>("Hash");
-
-                    b.Property<int>("ReferenceCount");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("Hash")
-                        .IsUnique();
-
-                    b.HasIndex("ReferenceCount");
-
-                    b.ToTable("FileInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Input.Bindings.DatabasedKeyBinding", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<int>("IntAction")
-                        .HasColumnName("Action");
-
-                    b.Property<string>("KeysString")
-                        .HasColumnName("Keys");
-
-                    b.Property<int?>("RulesetID");
-
-                    b.Property<int?>("Variant");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("IntAction");
-
-                    b.HasIndex("RulesetID", "Variant");
-
-                    b.ToTable("KeyBinding");
-                });
-
-            modelBuilder.Entity("osu.Game.Rulesets.RulesetInfo", b =>
-                {
-                    b.Property<int?>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<bool>("Available");
-
-                    b.Property<string>("InstantiationInfo");
-
-                    b.Property<string>("Name");
-
-                    b.Property<string>("ShortName");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("Available");
-
-                    b.HasIndex("ShortName")
-                        .IsUnique();
-
-                    b.ToTable("RulesetInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Scoring.ScoreFileInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<int>("FileInfoID");
-
-                    b.Property<string>("Filename")
-                        .IsRequired();
-
-                    b.Property<int?>("ScoreInfoID");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("FileInfoID");
-
-                    b.HasIndex("ScoreInfoID");
-
-                    b.ToTable("ScoreFileInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Scoring.ScoreInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<double>("Accuracy")
-                        .HasColumnType("DECIMAL(1,4)");
-
-                    b.Property<int>("BeatmapInfoID");
-
-                    b.Property<int>("Combo");
-
-                    b.Property<DateTimeOffset>("Date");
-
-                    b.Property<bool>("DeletePending");
-
-                    b.Property<string>("Hash");
-
-                    b.Property<int>("MaxCombo");
-
-                    b.Property<string>("ModsJson")
-                        .HasColumnName("Mods");
-
-                    b.Property<long?>("OnlineScoreID");
-
-                    b.Property<double?>("PP");
-
-                    b.Property<int>("Rank");
-
-                    b.Property<int>("RulesetID");
-
-                    b.Property<string>("StatisticsJson")
-                        .HasColumnName("Statistics");
-
-                    b.Property<long>("TotalScore");
-
-                    b.Property<long?>("UserID")
-                        .HasColumnName("UserID");
-
-                    b.Property<string>("UserString")
-                        .HasColumnName("User");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("BeatmapInfoID");
-
-                    b.HasIndex("OnlineScoreID")
-                        .IsUnique();
-
-                    b.HasIndex("RulesetID");
-
-                    b.ToTable("ScoreInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<int>("FileInfoID");
-
-                    b.Property<string>("Filename")
-                        .IsRequired();
-
-                    b.Property<int>("SkinInfoID");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("FileInfoID");
-
-                    b.HasIndex("SkinInfoID");
-
-                    b.ToTable("SkinFileInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Skinning.SkinInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<string>("Creator");
-
-                    b.Property<bool>("DeletePending");
-
-                    b.Property<string>("Hash");
-
-                    b.Property<string>("Name");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("DeletePending");
-
-                    b.HasIndex("Hash")
-                        .IsUnique();
-
-                    b.ToTable("SkinInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b =>
-                {
-                    b.HasOne("osu.Game.Beatmaps.BeatmapDifficulty", "BaseDifficulty")
-                        .WithMany()
-                        .HasForeignKey("BaseDifficultyID")
-                        .OnDelete(DeleteBehavior.Cascade);
-
-                    b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo", "BeatmapSet")
-                        .WithMany("Beatmaps")
-                        .HasForeignKey("BeatmapSetInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-
-                    b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata")
-                        .WithMany("Beatmaps")
-                        .HasForeignKey("MetadataID");
-
-                    b.HasOne("osu.Game.Rulesets.RulesetInfo", "Ruleset")
-                        .WithMany()
-                        .HasForeignKey("RulesetID")
-                        .OnDelete(DeleteBehavior.Cascade);
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b =>
-                {
-                    b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo")
-                        .WithMany("Files")
-                        .HasForeignKey("BeatmapSetInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-
-                    b.HasOne("osu.Game.IO.FileInfo", "FileInfo")
-                        .WithMany()
-                        .HasForeignKey("FileInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b =>
-                {
-                    b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata")
-                        .WithMany("BeatmapSets")
-                        .HasForeignKey("MetadataID");
-                });
-
-            modelBuilder.Entity("osu.Game.Configuration.DatabasedSetting", b =>
-                {
-                    b.HasOne("osu.Game.Skinning.SkinInfo")
-                        .WithMany("Settings")
-                        .HasForeignKey("SkinInfoID");
-                });
-
-            modelBuilder.Entity("osu.Game.Scoring.ScoreFileInfo", b =>
-                {
-                    b.HasOne("osu.Game.IO.FileInfo", "FileInfo")
-                        .WithMany()
-                        .HasForeignKey("FileInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-
-                    b.HasOne("osu.Game.Scoring.ScoreInfo")
-                        .WithMany("Files")
-                        .HasForeignKey("ScoreInfoID");
-                });
-
-            modelBuilder.Entity("osu.Game.Scoring.ScoreInfo", b =>
-                {
-                    b.HasOne("osu.Game.Beatmaps.BeatmapInfo", "Beatmap")
-                        .WithMany("Scores")
-                        .HasForeignKey("BeatmapInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-
-                    b.HasOne("osu.Game.Rulesets.RulesetInfo", "Ruleset")
-                        .WithMany()
-                        .HasForeignKey("RulesetID")
-                        .OnDelete(DeleteBehavior.Cascade);
-                });
-
-            modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b =>
-                {
-                    b.HasOne("osu.Game.IO.FileInfo", "FileInfo")
-                        .WithMany()
-                        .HasForeignKey("FileInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-
-                    b.HasOne("osu.Game.Skinning.SkinInfo")
-                        .WithMany("Files")
-                        .HasForeignKey("SkinInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-                });
-#pragma warning restore 612, 618
-        }
-    }
-}
diff --git a/osu.Game/Migrations/20190913104727_AddBeatmapVideo.cs b/osu.Game/Migrations/20190913104727_AddBeatmapVideo.cs
deleted file mode 100644
index 9ed0943acd..0000000000
--- a/osu.Game/Migrations/20190913104727_AddBeatmapVideo.cs
+++ /dev/null
@@ -1,22 +0,0 @@
-using Microsoft.EntityFrameworkCore.Migrations;
-
-namespace osu.Game.Migrations
-{
-    public partial class AddBeatmapVideo : Migration
-    {
-        protected override void Up(MigrationBuilder migrationBuilder)
-        {
-            migrationBuilder.AddColumn<string>(
-                name: "VideoFile",
-                table: "BeatmapMetadata",
-                nullable: true);
-        }
-
-        protected override void Down(MigrationBuilder migrationBuilder)
-        {
-            migrationBuilder.DropColumn(
-                name: "VideoFile",
-                table: "BeatmapMetadata");
-        }
-    }
-}
diff --git a/osu.Game/Migrations/20200302094919_RefreshVolumeBindings.Designer.cs b/osu.Game/Migrations/20200302094919_RefreshVolumeBindings.Designer.cs
deleted file mode 100644
index 22316b0380..0000000000
--- a/osu.Game/Migrations/20200302094919_RefreshVolumeBindings.Designer.cs
+++ /dev/null
@@ -1,506 +0,0 @@
-// <auto-generated />
-using System;
-using Microsoft.EntityFrameworkCore;
-using Microsoft.EntityFrameworkCore.Infrastructure;
-using Microsoft.EntityFrameworkCore.Migrations;
-using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
-using osu.Game.Database;
-
-namespace osu.Game.Migrations
-{
-    [DbContext(typeof(OsuDbContext))]
-    [Migration("20200302094919_RefreshVolumeBindings")]
-    partial class RefreshVolumeBindings
-    {
-        protected override void BuildTargetModel(ModelBuilder modelBuilder)
-        {
-#pragma warning disable 612, 618
-            modelBuilder
-                .HasAnnotation("ProductVersion", "2.2.6-servicing-10079");
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapDifficulty", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<float>("ApproachRate");
-
-                    b.Property<float>("CircleSize");
-
-                    b.Property<float>("DrainRate");
-
-                    b.Property<float>("OverallDifficulty");
-
-                    b.Property<double>("SliderMultiplier");
-
-                    b.Property<double>("SliderTickRate");
-
-                    b.HasKey("ID");
-
-                    b.ToTable("BeatmapDifficulty");
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<double>("AudioLeadIn");
-
-                    b.Property<double>("BPM");
-
-                    b.Property<int>("BaseDifficultyID");
-
-                    b.Property<int>("BeatDivisor");
-
-                    b.Property<int>("BeatmapSetInfoID");
-
-                    b.Property<bool>("Countdown");
-
-                    b.Property<double>("DistanceSpacing");
-
-                    b.Property<int>("GridSize");
-
-                    b.Property<string>("Hash");
-
-                    b.Property<bool>("Hidden");
-
-                    b.Property<double>("Length");
-
-                    b.Property<bool>("LetterboxInBreaks");
-
-                    b.Property<string>("MD5Hash");
-
-                    b.Property<int?>("MetadataID");
-
-                    b.Property<int?>("OnlineBeatmapID");
-
-                    b.Property<string>("Path");
-
-                    b.Property<int>("RulesetID");
-
-                    b.Property<bool>("SpecialStyle");
-
-                    b.Property<float>("StackLeniency");
-
-                    b.Property<double>("StarDifficulty");
-
-                    b.Property<int>("Status");
-
-                    b.Property<string>("StoredBookmarks");
-
-                    b.Property<double>("TimelineZoom");
-
-                    b.Property<string>("Version");
-
-                    b.Property<bool>("WidescreenStoryboard");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("BaseDifficultyID");
-
-                    b.HasIndex("BeatmapSetInfoID");
-
-                    b.HasIndex("Hash");
-
-                    b.HasIndex("MD5Hash");
-
-                    b.HasIndex("MetadataID");
-
-                    b.HasIndex("OnlineBeatmapID")
-                        .IsUnique();
-
-                    b.HasIndex("RulesetID");
-
-                    b.ToTable("BeatmapInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapMetadata", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<string>("Artist");
-
-                    b.Property<string>("ArtistUnicode");
-
-                    b.Property<string>("AudioFile");
-
-                    b.Property<string>("AuthorString")
-                        .HasColumnName("Author");
-
-                    b.Property<string>("BackgroundFile");
-
-                    b.Property<int>("PreviewTime");
-
-                    b.Property<string>("Source");
-
-                    b.Property<string>("Tags");
-
-                    b.Property<string>("Title");
-
-                    b.Property<string>("TitleUnicode");
-
-                    b.Property<string>("VideoFile");
-
-                    b.HasKey("ID");
-
-                    b.ToTable("BeatmapMetadata");
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<int>("BeatmapSetInfoID");
-
-                    b.Property<int>("FileInfoID");
-
-                    b.Property<string>("Filename")
-                        .IsRequired();
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("BeatmapSetInfoID");
-
-                    b.HasIndex("FileInfoID");
-
-                    b.ToTable("BeatmapSetFileInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<DateTimeOffset>("DateAdded");
-
-                    b.Property<bool>("DeletePending");
-
-                    b.Property<string>("Hash");
-
-                    b.Property<int?>("MetadataID");
-
-                    b.Property<int?>("OnlineBeatmapSetID");
-
-                    b.Property<bool>("Protected");
-
-                    b.Property<int>("Status");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("DeletePending");
-
-                    b.HasIndex("Hash")
-                        .IsUnique();
-
-                    b.HasIndex("MetadataID");
-
-                    b.HasIndex("OnlineBeatmapSetID")
-                        .IsUnique();
-
-                    b.ToTable("BeatmapSetInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Configuration.DatabasedSetting", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<string>("Key")
-                        .HasColumnName("Key");
-
-                    b.Property<int?>("RulesetID");
-
-                    b.Property<int?>("SkinInfoID");
-
-                    b.Property<string>("StringValue")
-                        .HasColumnName("Value");
-
-                    b.Property<int?>("Variant");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("SkinInfoID");
-
-                    b.HasIndex("RulesetID", "Variant");
-
-                    b.ToTable("Settings");
-                });
-
-            modelBuilder.Entity("osu.Game.IO.FileInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<string>("Hash");
-
-                    b.Property<int>("ReferenceCount");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("Hash")
-                        .IsUnique();
-
-                    b.HasIndex("ReferenceCount");
-
-                    b.ToTable("FileInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Input.Bindings.DatabasedKeyBinding", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<int>("IntAction")
-                        .HasColumnName("Action");
-
-                    b.Property<string>("KeysString")
-                        .HasColumnName("Keys");
-
-                    b.Property<int?>("RulesetID");
-
-                    b.Property<int?>("Variant");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("IntAction");
-
-                    b.HasIndex("RulesetID", "Variant");
-
-                    b.ToTable("KeyBinding");
-                });
-
-            modelBuilder.Entity("osu.Game.Rulesets.RulesetInfo", b =>
-                {
-                    b.Property<int?>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<bool>("Available");
-
-                    b.Property<string>("InstantiationInfo");
-
-                    b.Property<string>("Name");
-
-                    b.Property<string>("ShortName");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("Available");
-
-                    b.HasIndex("ShortName")
-                        .IsUnique();
-
-                    b.ToTable("RulesetInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Scoring.ScoreFileInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<int>("FileInfoID");
-
-                    b.Property<string>("Filename")
-                        .IsRequired();
-
-                    b.Property<int?>("ScoreInfoID");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("FileInfoID");
-
-                    b.HasIndex("ScoreInfoID");
-
-                    b.ToTable("ScoreFileInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Scoring.ScoreInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<double>("Accuracy")
-                        .HasColumnType("DECIMAL(1,4)");
-
-                    b.Property<int>("BeatmapInfoID");
-
-                    b.Property<int>("Combo");
-
-                    b.Property<DateTimeOffset>("Date");
-
-                    b.Property<bool>("DeletePending");
-
-                    b.Property<string>("Hash");
-
-                    b.Property<int>("MaxCombo");
-
-                    b.Property<string>("ModsJson")
-                        .HasColumnName("Mods");
-
-                    b.Property<long?>("OnlineScoreID");
-
-                    b.Property<double?>("PP");
-
-                    b.Property<int>("Rank");
-
-                    b.Property<int>("RulesetID");
-
-                    b.Property<string>("StatisticsJson")
-                        .HasColumnName("Statistics");
-
-                    b.Property<long>("TotalScore");
-
-                    b.Property<long?>("UserID")
-                        .HasColumnName("UserID");
-
-                    b.Property<string>("UserString")
-                        .HasColumnName("User");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("BeatmapInfoID");
-
-                    b.HasIndex("OnlineScoreID")
-                        .IsUnique();
-
-                    b.HasIndex("RulesetID");
-
-                    b.ToTable("ScoreInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<int>("FileInfoID");
-
-                    b.Property<string>("Filename")
-                        .IsRequired();
-
-                    b.Property<int>("SkinInfoID");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("FileInfoID");
-
-                    b.HasIndex("SkinInfoID");
-
-                    b.ToTable("SkinFileInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Skinning.SkinInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<string>("Creator");
-
-                    b.Property<bool>("DeletePending");
-
-                    b.Property<string>("Hash");
-
-                    b.Property<string>("Name");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("DeletePending");
-
-                    b.HasIndex("Hash")
-                        .IsUnique();
-
-                    b.ToTable("SkinInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b =>
-                {
-                    b.HasOne("osu.Game.Beatmaps.BeatmapDifficulty", "BaseDifficulty")
-                        .WithMany()
-                        .HasForeignKey("BaseDifficultyID")
-                        .OnDelete(DeleteBehavior.Cascade);
-
-                    b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo", "BeatmapSet")
-                        .WithMany("Beatmaps")
-                        .HasForeignKey("BeatmapSetInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-
-                    b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata")
-                        .WithMany("Beatmaps")
-                        .HasForeignKey("MetadataID");
-
-                    b.HasOne("osu.Game.Rulesets.RulesetInfo", "Ruleset")
-                        .WithMany()
-                        .HasForeignKey("RulesetID")
-                        .OnDelete(DeleteBehavior.Cascade);
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b =>
-                {
-                    b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo")
-                        .WithMany("Files")
-                        .HasForeignKey("BeatmapSetInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-
-                    b.HasOne("osu.Game.IO.FileInfo", "FileInfo")
-                        .WithMany()
-                        .HasForeignKey("FileInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b =>
-                {
-                    b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata")
-                        .WithMany("BeatmapSets")
-                        .HasForeignKey("MetadataID");
-                });
-
-            modelBuilder.Entity("osu.Game.Configuration.DatabasedSetting", b =>
-                {
-                    b.HasOne("osu.Game.Skinning.SkinInfo")
-                        .WithMany("Settings")
-                        .HasForeignKey("SkinInfoID");
-                });
-
-            modelBuilder.Entity("osu.Game.Scoring.ScoreFileInfo", b =>
-                {
-                    b.HasOne("osu.Game.IO.FileInfo", "FileInfo")
-                        .WithMany()
-                        .HasForeignKey("FileInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-
-                    b.HasOne("osu.Game.Scoring.ScoreInfo")
-                        .WithMany("Files")
-                        .HasForeignKey("ScoreInfoID");
-                });
-
-            modelBuilder.Entity("osu.Game.Scoring.ScoreInfo", b =>
-                {
-                    b.HasOne("osu.Game.Beatmaps.BeatmapInfo", "Beatmap")
-                        .WithMany("Scores")
-                        .HasForeignKey("BeatmapInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-
-                    b.HasOne("osu.Game.Rulesets.RulesetInfo", "Ruleset")
-                        .WithMany()
-                        .HasForeignKey("RulesetID")
-                        .OnDelete(DeleteBehavior.Cascade);
-                });
-
-            modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b =>
-                {
-                    b.HasOne("osu.Game.IO.FileInfo", "FileInfo")
-                        .WithMany()
-                        .HasForeignKey("FileInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-
-                    b.HasOne("osu.Game.Skinning.SkinInfo")
-                        .WithMany("Files")
-                        .HasForeignKey("SkinInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-                });
-#pragma warning restore 612, 618
-        }
-    }
-}
diff --git a/osu.Game/Migrations/20200302094919_RefreshVolumeBindings.cs b/osu.Game/Migrations/20200302094919_RefreshVolumeBindings.cs
deleted file mode 100644
index ec4475971c..0000000000
--- a/osu.Game/Migrations/20200302094919_RefreshVolumeBindings.cs
+++ /dev/null
@@ -1,16 +0,0 @@
-using Microsoft.EntityFrameworkCore.Migrations;
-
-namespace osu.Game.Migrations
-{
-    public partial class RefreshVolumeBindings : Migration
-    {
-        protected override void Up(MigrationBuilder migrationBuilder)
-        {
-            migrationBuilder.Sql("DELETE FROM KeyBinding WHERE action in (6,7)");
-        }
-
-        protected override void Down(MigrationBuilder migrationBuilder)
-        {
-        }
-    }
-}
diff --git a/osu.Game/Migrations/20201019224408_AddEpilepsyWarning.Designer.cs b/osu.Game/Migrations/20201019224408_AddEpilepsyWarning.Designer.cs
deleted file mode 100644
index 1c05de832e..0000000000
--- a/osu.Game/Migrations/20201019224408_AddEpilepsyWarning.Designer.cs
+++ /dev/null
@@ -1,508 +0,0 @@
-// <auto-generated />
-using System;
-using Microsoft.EntityFrameworkCore;
-using Microsoft.EntityFrameworkCore.Infrastructure;
-using Microsoft.EntityFrameworkCore.Migrations;
-using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
-using osu.Game.Database;
-
-namespace osu.Game.Migrations
-{
-    [DbContext(typeof(OsuDbContext))]
-    [Migration("20201019224408_AddEpilepsyWarning")]
-    partial class AddEpilepsyWarning
-    {
-        protected override void BuildTargetModel(ModelBuilder modelBuilder)
-        {
-#pragma warning disable 612, 618
-            modelBuilder
-                .HasAnnotation("ProductVersion", "2.2.6-servicing-10079");
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapDifficulty", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<float>("ApproachRate");
-
-                    b.Property<float>("CircleSize");
-
-                    b.Property<float>("DrainRate");
-
-                    b.Property<float>("OverallDifficulty");
-
-                    b.Property<double>("SliderMultiplier");
-
-                    b.Property<double>("SliderTickRate");
-
-                    b.HasKey("ID");
-
-                    b.ToTable("BeatmapDifficulty");
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<double>("AudioLeadIn");
-
-                    b.Property<double>("BPM");
-
-                    b.Property<int>("BaseDifficultyID");
-
-                    b.Property<int>("BeatDivisor");
-
-                    b.Property<int>("BeatmapSetInfoID");
-
-                    b.Property<bool>("Countdown");
-
-                    b.Property<double>("DistanceSpacing");
-
-                    b.Property<bool>("EpilepsyWarning");
-
-                    b.Property<int>("GridSize");
-
-                    b.Property<string>("Hash");
-
-                    b.Property<bool>("Hidden");
-
-                    b.Property<double>("Length");
-
-                    b.Property<bool>("LetterboxInBreaks");
-
-                    b.Property<string>("MD5Hash");
-
-                    b.Property<int?>("MetadataID");
-
-                    b.Property<int?>("OnlineBeatmapID");
-
-                    b.Property<string>("Path");
-
-                    b.Property<int>("RulesetID");
-
-                    b.Property<bool>("SpecialStyle");
-
-                    b.Property<float>("StackLeniency");
-
-                    b.Property<double>("StarDifficulty");
-
-                    b.Property<int>("Status");
-
-                    b.Property<string>("StoredBookmarks");
-
-                    b.Property<double>("TimelineZoom");
-
-                    b.Property<string>("Version");
-
-                    b.Property<bool>("WidescreenStoryboard");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("BaseDifficultyID");
-
-                    b.HasIndex("BeatmapSetInfoID");
-
-                    b.HasIndex("Hash");
-
-                    b.HasIndex("MD5Hash");
-
-                    b.HasIndex("MetadataID");
-
-                    b.HasIndex("OnlineBeatmapID")
-                        .IsUnique();
-
-                    b.HasIndex("RulesetID");
-
-                    b.ToTable("BeatmapInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapMetadata", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<string>("Artist");
-
-                    b.Property<string>("ArtistUnicode");
-
-                    b.Property<string>("AudioFile");
-
-                    b.Property<string>("AuthorString")
-                        .HasColumnName("Author");
-
-                    b.Property<string>("BackgroundFile");
-
-                    b.Property<int>("PreviewTime");
-
-                    b.Property<string>("Source");
-
-                    b.Property<string>("Tags");
-
-                    b.Property<string>("Title");
-
-                    b.Property<string>("TitleUnicode");
-
-                    b.Property<string>("VideoFile");
-
-                    b.HasKey("ID");
-
-                    b.ToTable("BeatmapMetadata");
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<int>("BeatmapSetInfoID");
-
-                    b.Property<int>("FileInfoID");
-
-                    b.Property<string>("Filename")
-                        .IsRequired();
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("BeatmapSetInfoID");
-
-                    b.HasIndex("FileInfoID");
-
-                    b.ToTable("BeatmapSetFileInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<DateTimeOffset>("DateAdded");
-
-                    b.Property<bool>("DeletePending");
-
-                    b.Property<string>("Hash");
-
-                    b.Property<int?>("MetadataID");
-
-                    b.Property<int?>("OnlineBeatmapSetID");
-
-                    b.Property<bool>("Protected");
-
-                    b.Property<int>("Status");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("DeletePending");
-
-                    b.HasIndex("Hash")
-                        .IsUnique();
-
-                    b.HasIndex("MetadataID");
-
-                    b.HasIndex("OnlineBeatmapSetID")
-                        .IsUnique();
-
-                    b.ToTable("BeatmapSetInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Configuration.DatabasedSetting", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<string>("Key")
-                        .HasColumnName("Key");
-
-                    b.Property<int?>("RulesetID");
-
-                    b.Property<int?>("SkinInfoID");
-
-                    b.Property<string>("StringValue")
-                        .HasColumnName("Value");
-
-                    b.Property<int?>("Variant");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("SkinInfoID");
-
-                    b.HasIndex("RulesetID", "Variant");
-
-                    b.ToTable("Settings");
-                });
-
-            modelBuilder.Entity("osu.Game.IO.FileInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<string>("Hash");
-
-                    b.Property<int>("ReferenceCount");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("Hash")
-                        .IsUnique();
-
-                    b.HasIndex("ReferenceCount");
-
-                    b.ToTable("FileInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Input.Bindings.DatabasedKeyBinding", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<int>("IntAction")
-                        .HasColumnName("Action");
-
-                    b.Property<string>("KeysString")
-                        .HasColumnName("Keys");
-
-                    b.Property<int?>("RulesetID");
-
-                    b.Property<int?>("Variant");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("IntAction");
-
-                    b.HasIndex("RulesetID", "Variant");
-
-                    b.ToTable("KeyBinding");
-                });
-
-            modelBuilder.Entity("osu.Game.Rulesets.RulesetInfo", b =>
-                {
-                    b.Property<int?>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<bool>("Available");
-
-                    b.Property<string>("InstantiationInfo");
-
-                    b.Property<string>("Name");
-
-                    b.Property<string>("ShortName");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("Available");
-
-                    b.HasIndex("ShortName")
-                        .IsUnique();
-
-                    b.ToTable("RulesetInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Scoring.ScoreFileInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<int>("FileInfoID");
-
-                    b.Property<string>("Filename")
-                        .IsRequired();
-
-                    b.Property<int?>("ScoreInfoID");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("FileInfoID");
-
-                    b.HasIndex("ScoreInfoID");
-
-                    b.ToTable("ScoreFileInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Scoring.ScoreInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<double>("Accuracy")
-                        .HasColumnType("DECIMAL(1,4)");
-
-                    b.Property<int>("BeatmapInfoID");
-
-                    b.Property<int>("Combo");
-
-                    b.Property<DateTimeOffset>("Date");
-
-                    b.Property<bool>("DeletePending");
-
-                    b.Property<string>("Hash");
-
-                    b.Property<int>("MaxCombo");
-
-                    b.Property<string>("ModsJson")
-                        .HasColumnName("Mods");
-
-                    b.Property<long?>("OnlineScoreID");
-
-                    b.Property<double?>("PP");
-
-                    b.Property<int>("Rank");
-
-                    b.Property<int>("RulesetID");
-
-                    b.Property<string>("StatisticsJson")
-                        .HasColumnName("Statistics");
-
-                    b.Property<long>("TotalScore");
-
-                    b.Property<long?>("UserID")
-                        .HasColumnName("UserID");
-
-                    b.Property<string>("UserString")
-                        .HasColumnName("User");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("BeatmapInfoID");
-
-                    b.HasIndex("OnlineScoreID")
-                        .IsUnique();
-
-                    b.HasIndex("RulesetID");
-
-                    b.ToTable("ScoreInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<int>("FileInfoID");
-
-                    b.Property<string>("Filename")
-                        .IsRequired();
-
-                    b.Property<int>("SkinInfoID");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("FileInfoID");
-
-                    b.HasIndex("SkinInfoID");
-
-                    b.ToTable("SkinFileInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Skinning.SkinInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<string>("Creator");
-
-                    b.Property<bool>("DeletePending");
-
-                    b.Property<string>("Hash");
-
-                    b.Property<string>("Name");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("DeletePending");
-
-                    b.HasIndex("Hash")
-                        .IsUnique();
-
-                    b.ToTable("SkinInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b =>
-                {
-                    b.HasOne("osu.Game.Beatmaps.BeatmapDifficulty", "BaseDifficulty")
-                        .WithMany()
-                        .HasForeignKey("BaseDifficultyID")
-                        .OnDelete(DeleteBehavior.Cascade);
-
-                    b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo", "BeatmapSet")
-                        .WithMany("Beatmaps")
-                        .HasForeignKey("BeatmapSetInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-
-                    b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata")
-                        .WithMany("Beatmaps")
-                        .HasForeignKey("MetadataID");
-
-                    b.HasOne("osu.Game.Rulesets.RulesetInfo", "Ruleset")
-                        .WithMany()
-                        .HasForeignKey("RulesetID")
-                        .OnDelete(DeleteBehavior.Cascade);
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b =>
-                {
-                    b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo")
-                        .WithMany("Files")
-                        .HasForeignKey("BeatmapSetInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-
-                    b.HasOne("osu.Game.IO.FileInfo", "FileInfo")
-                        .WithMany()
-                        .HasForeignKey("FileInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b =>
-                {
-                    b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata")
-                        .WithMany("BeatmapSets")
-                        .HasForeignKey("MetadataID");
-                });
-
-            modelBuilder.Entity("osu.Game.Configuration.DatabasedSetting", b =>
-                {
-                    b.HasOne("osu.Game.Skinning.SkinInfo")
-                        .WithMany("Settings")
-                        .HasForeignKey("SkinInfoID");
-                });
-
-            modelBuilder.Entity("osu.Game.Scoring.ScoreFileInfo", b =>
-                {
-                    b.HasOne("osu.Game.IO.FileInfo", "FileInfo")
-                        .WithMany()
-                        .HasForeignKey("FileInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-
-                    b.HasOne("osu.Game.Scoring.ScoreInfo")
-                        .WithMany("Files")
-                        .HasForeignKey("ScoreInfoID");
-                });
-
-            modelBuilder.Entity("osu.Game.Scoring.ScoreInfo", b =>
-                {
-                    b.HasOne("osu.Game.Beatmaps.BeatmapInfo", "Beatmap")
-                        .WithMany("Scores")
-                        .HasForeignKey("BeatmapInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-
-                    b.HasOne("osu.Game.Rulesets.RulesetInfo", "Ruleset")
-                        .WithMany()
-                        .HasForeignKey("RulesetID")
-                        .OnDelete(DeleteBehavior.Cascade);
-                });
-
-            modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b =>
-                {
-                    b.HasOne("osu.Game.IO.FileInfo", "FileInfo")
-                        .WithMany()
-                        .HasForeignKey("FileInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-
-                    b.HasOne("osu.Game.Skinning.SkinInfo")
-                        .WithMany("Files")
-                        .HasForeignKey("SkinInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-                });
-#pragma warning restore 612, 618
-        }
-    }
-}
diff --git a/osu.Game/Migrations/20201019224408_AddEpilepsyWarning.cs b/osu.Game/Migrations/20201019224408_AddEpilepsyWarning.cs
deleted file mode 100644
index be6968aa5d..0000000000
--- a/osu.Game/Migrations/20201019224408_AddEpilepsyWarning.cs
+++ /dev/null
@@ -1,23 +0,0 @@
-using Microsoft.EntityFrameworkCore.Migrations;
-
-namespace osu.Game.Migrations
-{
-    public partial class AddEpilepsyWarning : Migration
-    {
-        protected override void Up(MigrationBuilder migrationBuilder)
-        {
-            migrationBuilder.AddColumn<bool>(
-                name: "EpilepsyWarning",
-                table: "BeatmapInfo",
-                nullable: false,
-                defaultValue: false);
-        }
-
-        protected override void Down(MigrationBuilder migrationBuilder)
-        {
-            migrationBuilder.DropColumn(
-                name: "EpilepsyWarning",
-                table: "BeatmapInfo");
-        }
-    }
-}
diff --git a/osu.Game/Migrations/20210412045700_RefreshVolumeBindingsAgain.Designer.cs b/osu.Game/Migrations/20210412045700_RefreshVolumeBindingsAgain.Designer.cs
deleted file mode 100644
index 2c100d39b9..0000000000
--- a/osu.Game/Migrations/20210412045700_RefreshVolumeBindingsAgain.Designer.cs
+++ /dev/null
@@ -1,506 +0,0 @@
-// <auto-generated />
-using System;
-using Microsoft.EntityFrameworkCore;
-using Microsoft.EntityFrameworkCore.Infrastructure;
-using Microsoft.EntityFrameworkCore.Migrations;
-using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
-using osu.Game.Database;
-
-namespace osu.Game.Migrations
-{
-    [DbContext(typeof(OsuDbContext))]
-    [Migration("20210412045700_RefreshVolumeBindingsAgain")]
-    partial class RefreshVolumeBindingsAgain
-    {
-        protected override void BuildTargetModel(ModelBuilder modelBuilder)
-        {
-#pragma warning disable 612, 618
-            modelBuilder
-                .HasAnnotation("ProductVersion", "2.2.6-servicing-10079");
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapDifficulty", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<float>("ApproachRate");
-
-                    b.Property<float>("CircleSize");
-
-                    b.Property<float>("DrainRate");
-
-                    b.Property<float>("OverallDifficulty");
-
-                    b.Property<double>("SliderMultiplier");
-
-                    b.Property<double>("SliderTickRate");
-
-                    b.HasKey("ID");
-
-                    b.ToTable("BeatmapDifficulty");
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<double>("AudioLeadIn");
-
-                    b.Property<double>("BPM");
-
-                    b.Property<int>("BaseDifficultyID");
-
-                    b.Property<int>("BeatDivisor");
-
-                    b.Property<int>("BeatmapSetInfoID");
-
-                    b.Property<bool>("Countdown");
-
-                    b.Property<double>("DistanceSpacing");
-
-                    b.Property<int>("GridSize");
-
-                    b.Property<string>("Hash");
-
-                    b.Property<bool>("Hidden");
-
-                    b.Property<double>("Length");
-
-                    b.Property<bool>("LetterboxInBreaks");
-
-                    b.Property<string>("MD5Hash");
-
-                    b.Property<int?>("MetadataID");
-
-                    b.Property<int?>("OnlineBeatmapID");
-
-                    b.Property<string>("Path");
-
-                    b.Property<int>("RulesetID");
-
-                    b.Property<bool>("SpecialStyle");
-
-                    b.Property<float>("StackLeniency");
-
-                    b.Property<double>("StarDifficulty");
-
-                    b.Property<int>("Status");
-
-                    b.Property<string>("StoredBookmarks");
-
-                    b.Property<double>("TimelineZoom");
-
-                    b.Property<string>("Version");
-
-                    b.Property<bool>("WidescreenStoryboard");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("BaseDifficultyID");
-
-                    b.HasIndex("BeatmapSetInfoID");
-
-                    b.HasIndex("Hash");
-
-                    b.HasIndex("MD5Hash");
-
-                    b.HasIndex("MetadataID");
-
-                    b.HasIndex("OnlineBeatmapID")
-                        .IsUnique();
-
-                    b.HasIndex("RulesetID");
-
-                    b.ToTable("BeatmapInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapMetadata", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<string>("Artist");
-
-                    b.Property<string>("ArtistUnicode");
-
-                    b.Property<string>("AudioFile");
-
-                    b.Property<string>("AuthorString")
-                        .HasColumnName("Author");
-
-                    b.Property<string>("BackgroundFile");
-
-                    b.Property<int>("PreviewTime");
-
-                    b.Property<string>("Source");
-
-                    b.Property<string>("Tags");
-
-                    b.Property<string>("Title");
-
-                    b.Property<string>("TitleUnicode");
-
-                    b.Property<string>("VideoFile");
-
-                    b.HasKey("ID");
-
-                    b.ToTable("BeatmapMetadata");
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<int>("BeatmapSetInfoID");
-
-                    b.Property<int>("FileInfoID");
-
-                    b.Property<string>("Filename")
-                        .IsRequired();
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("BeatmapSetInfoID");
-
-                    b.HasIndex("FileInfoID");
-
-                    b.ToTable("BeatmapSetFileInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<DateTimeOffset>("DateAdded");
-
-                    b.Property<bool>("DeletePending");
-
-                    b.Property<string>("Hash");
-
-                    b.Property<int?>("MetadataID");
-
-                    b.Property<int?>("OnlineBeatmapSetID");
-
-                    b.Property<bool>("Protected");
-
-                    b.Property<int>("Status");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("DeletePending");
-
-                    b.HasIndex("Hash")
-                        .IsUnique();
-
-                    b.HasIndex("MetadataID");
-
-                    b.HasIndex("OnlineBeatmapSetID")
-                        .IsUnique();
-
-                    b.ToTable("BeatmapSetInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Configuration.DatabasedSetting", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<string>("Key")
-                        .HasColumnName("Key");
-
-                    b.Property<int?>("RulesetID");
-
-                    b.Property<int?>("SkinInfoID");
-
-                    b.Property<string>("StringValue")
-                        .HasColumnName("Value");
-
-                    b.Property<int?>("Variant");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("SkinInfoID");
-
-                    b.HasIndex("RulesetID", "Variant");
-
-                    b.ToTable("Settings");
-                });
-
-            modelBuilder.Entity("osu.Game.IO.FileInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<string>("Hash");
-
-                    b.Property<int>("ReferenceCount");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("Hash")
-                        .IsUnique();
-
-                    b.HasIndex("ReferenceCount");
-
-                    b.ToTable("FileInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Input.Bindings.DatabasedKeyBinding", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<int>("IntAction")
-                        .HasColumnName("Action");
-
-                    b.Property<string>("KeysString")
-                        .HasColumnName("Keys");
-
-                    b.Property<int?>("RulesetID");
-
-                    b.Property<int?>("Variant");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("IntAction");
-
-                    b.HasIndex("RulesetID", "Variant");
-
-                    b.ToTable("KeyBinding");
-                });
-
-            modelBuilder.Entity("osu.Game.Rulesets.RulesetInfo", b =>
-                {
-                    b.Property<int?>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<bool>("Available");
-
-                    b.Property<string>("InstantiationInfo");
-
-                    b.Property<string>("Name");
-
-                    b.Property<string>("ShortName");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("Available");
-
-                    b.HasIndex("ShortName")
-                        .IsUnique();
-
-                    b.ToTable("RulesetInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Scoring.ScoreFileInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<int>("FileInfoID");
-
-                    b.Property<string>("Filename")
-                        .IsRequired();
-
-                    b.Property<int?>("ScoreInfoID");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("FileInfoID");
-
-                    b.HasIndex("ScoreInfoID");
-
-                    b.ToTable("ScoreFileInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Scoring.ScoreInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<double>("Accuracy")
-                        .HasColumnType("DECIMAL(1,4)");
-
-                    b.Property<int>("BeatmapInfoID");
-
-                    b.Property<int>("Combo");
-
-                    b.Property<DateTimeOffset>("Date");
-
-                    b.Property<bool>("DeletePending");
-
-                    b.Property<string>("Hash");
-
-                    b.Property<int>("MaxCombo");
-
-                    b.Property<string>("ModsJson")
-                        .HasColumnName("Mods");
-
-                    b.Property<long?>("OnlineScoreID");
-
-                    b.Property<double?>("PP");
-
-                    b.Property<int>("Rank");
-
-                    b.Property<int>("RulesetID");
-
-                    b.Property<string>("StatisticsJson")
-                        .HasColumnName("Statistics");
-
-                    b.Property<long>("TotalScore");
-
-                    b.Property<long?>("UserID")
-                        .HasColumnName("UserID");
-
-                    b.Property<string>("UserString")
-                        .HasColumnName("User");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("BeatmapInfoID");
-
-                    b.HasIndex("OnlineScoreID")
-                        .IsUnique();
-
-                    b.HasIndex("RulesetID");
-
-                    b.ToTable("ScoreInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<int>("FileInfoID");
-
-                    b.Property<string>("Filename")
-                        .IsRequired();
-
-                    b.Property<int>("SkinInfoID");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("FileInfoID");
-
-                    b.HasIndex("SkinInfoID");
-
-                    b.ToTable("SkinFileInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Skinning.SkinInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<string>("Creator");
-
-                    b.Property<bool>("DeletePending");
-
-                    b.Property<string>("Hash");
-
-                    b.Property<string>("Name");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("DeletePending");
-
-                    b.HasIndex("Hash")
-                        .IsUnique();
-
-                    b.ToTable("SkinInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b =>
-                {
-                    b.HasOne("osu.Game.Beatmaps.BeatmapDifficulty", "BaseDifficulty")
-                        .WithMany()
-                        .HasForeignKey("BaseDifficultyID")
-                        .OnDelete(DeleteBehavior.Cascade);
-
-                    b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo", "BeatmapSet")
-                        .WithMany("Beatmaps")
-                        .HasForeignKey("BeatmapSetInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-
-                    b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata")
-                        .WithMany("Beatmaps")
-                        .HasForeignKey("MetadataID");
-
-                    b.HasOne("osu.Game.Rulesets.RulesetInfo", "Ruleset")
-                        .WithMany()
-                        .HasForeignKey("RulesetID")
-                        .OnDelete(DeleteBehavior.Cascade);
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b =>
-                {
-                    b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo")
-                        .WithMany("Files")
-                        .HasForeignKey("BeatmapSetInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-
-                    b.HasOne("osu.Game.IO.FileInfo", "FileInfo")
-                        .WithMany()
-                        .HasForeignKey("FileInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b =>
-                {
-                    b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata")
-                        .WithMany("BeatmapSets")
-                        .HasForeignKey("MetadataID");
-                });
-
-            modelBuilder.Entity("osu.Game.Configuration.DatabasedSetting", b =>
-                {
-                    b.HasOne("osu.Game.Skinning.SkinInfo")
-                        .WithMany("Settings")
-                        .HasForeignKey("SkinInfoID");
-                });
-
-            modelBuilder.Entity("osu.Game.Scoring.ScoreFileInfo", b =>
-                {
-                    b.HasOne("osu.Game.IO.FileInfo", "FileInfo")
-                        .WithMany()
-                        .HasForeignKey("FileInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-
-                    b.HasOne("osu.Game.Scoring.ScoreInfo")
-                        .WithMany("Files")
-                        .HasForeignKey("ScoreInfoID");
-                });
-
-            modelBuilder.Entity("osu.Game.Scoring.ScoreInfo", b =>
-                {
-                    b.HasOne("osu.Game.Beatmaps.BeatmapInfo", "Beatmap")
-                        .WithMany("Scores")
-                        .HasForeignKey("BeatmapInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-
-                    b.HasOne("osu.Game.Rulesets.RulesetInfo", "Ruleset")
-                        .WithMany()
-                        .HasForeignKey("RulesetID")
-                        .OnDelete(DeleteBehavior.Cascade);
-                });
-
-            modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b =>
-                {
-                    b.HasOne("osu.Game.IO.FileInfo", "FileInfo")
-                        .WithMany()
-                        .HasForeignKey("FileInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-
-                    b.HasOne("osu.Game.Skinning.SkinInfo")
-                        .WithMany("Files")
-                        .HasForeignKey("SkinInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-                });
-#pragma warning restore 612, 618
-        }
-    }
-}
diff --git a/osu.Game/Migrations/20210412045700_RefreshVolumeBindingsAgain.cs b/osu.Game/Migrations/20210412045700_RefreshVolumeBindingsAgain.cs
deleted file mode 100644
index 155d6670a8..0000000000
--- a/osu.Game/Migrations/20210412045700_RefreshVolumeBindingsAgain.cs
+++ /dev/null
@@ -1,16 +0,0 @@
-using Microsoft.EntityFrameworkCore.Migrations;
-
-namespace osu.Game.Migrations
-{
-    public partial class RefreshVolumeBindingsAgain : Migration
-    {
-        protected override void Up(MigrationBuilder migrationBuilder)
-        {
-            migrationBuilder.Sql("DELETE FROM KeyBinding WHERE action in (6,7)");
-        }
-
-        protected override void Down(MigrationBuilder migrationBuilder)
-        {
-        }
-    }
-}
diff --git a/osu.Game/Migrations/20210511060743_AddSkinInstantiationInfo.Designer.cs b/osu.Game/Migrations/20210511060743_AddSkinInstantiationInfo.Designer.cs
deleted file mode 100644
index b808c648da..0000000000
--- a/osu.Game/Migrations/20210511060743_AddSkinInstantiationInfo.Designer.cs
+++ /dev/null
@@ -1,508 +0,0 @@
-// <auto-generated />
-using System;
-using Microsoft.EntityFrameworkCore;
-using Microsoft.EntityFrameworkCore.Infrastructure;
-using Microsoft.EntityFrameworkCore.Migrations;
-using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
-using osu.Game.Database;
-
-namespace osu.Game.Migrations
-{
-    [DbContext(typeof(OsuDbContext))]
-    [Migration("20210511060743_AddSkinInstantiationInfo")]
-    partial class AddSkinInstantiationInfo
-    {
-        protected override void BuildTargetModel(ModelBuilder modelBuilder)
-        {
-#pragma warning disable 612, 618
-            modelBuilder
-                .HasAnnotation("ProductVersion", "2.2.6-servicing-10079");
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapDifficulty", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<float>("ApproachRate");
-
-                    b.Property<float>("CircleSize");
-
-                    b.Property<float>("DrainRate");
-
-                    b.Property<float>("OverallDifficulty");
-
-                    b.Property<double>("SliderMultiplier");
-
-                    b.Property<double>("SliderTickRate");
-
-                    b.HasKey("ID");
-
-                    b.ToTable("BeatmapDifficulty");
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<double>("AudioLeadIn");
-
-                    b.Property<double>("BPM");
-
-                    b.Property<int>("BaseDifficultyID");
-
-                    b.Property<int>("BeatDivisor");
-
-                    b.Property<int>("BeatmapSetInfoID");
-
-                    b.Property<bool>("Countdown");
-
-                    b.Property<double>("DistanceSpacing");
-
-                    b.Property<bool>("EpilepsyWarning");
-
-                    b.Property<int>("GridSize");
-
-                    b.Property<string>("Hash");
-
-                    b.Property<bool>("Hidden");
-
-                    b.Property<double>("Length");
-
-                    b.Property<bool>("LetterboxInBreaks");
-
-                    b.Property<string>("MD5Hash");
-
-                    b.Property<int?>("MetadataID");
-
-                    b.Property<int?>("OnlineBeatmapID");
-
-                    b.Property<string>("Path");
-
-                    b.Property<int>("RulesetID");
-
-                    b.Property<bool>("SpecialStyle");
-
-                    b.Property<float>("StackLeniency");
-
-                    b.Property<double>("StarDifficulty");
-
-                    b.Property<int>("Status");
-
-                    b.Property<string>("StoredBookmarks");
-
-                    b.Property<double>("TimelineZoom");
-
-                    b.Property<string>("Version");
-
-                    b.Property<bool>("WidescreenStoryboard");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("BaseDifficultyID");
-
-                    b.HasIndex("BeatmapSetInfoID");
-
-                    b.HasIndex("Hash");
-
-                    b.HasIndex("MD5Hash");
-
-                    b.HasIndex("MetadataID");
-
-                    b.HasIndex("OnlineBeatmapID")
-                        .IsUnique();
-
-                    b.HasIndex("RulesetID");
-
-                    b.ToTable("BeatmapInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapMetadata", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<string>("Artist");
-
-                    b.Property<string>("ArtistUnicode");
-
-                    b.Property<string>("AudioFile");
-
-                    b.Property<string>("AuthorString")
-                        .HasColumnName("Author");
-
-                    b.Property<string>("BackgroundFile");
-
-                    b.Property<int>("PreviewTime");
-
-                    b.Property<string>("Source");
-
-                    b.Property<string>("Tags");
-
-                    b.Property<string>("Title");
-
-                    b.Property<string>("TitleUnicode");
-
-                    b.HasKey("ID");
-
-                    b.ToTable("BeatmapMetadata");
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<int>("BeatmapSetInfoID");
-
-                    b.Property<int>("FileInfoID");
-
-                    b.Property<string>("Filename")
-                        .IsRequired();
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("BeatmapSetInfoID");
-
-                    b.HasIndex("FileInfoID");
-
-                    b.ToTable("BeatmapSetFileInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<DateTimeOffset>("DateAdded");
-
-                    b.Property<bool>("DeletePending");
-
-                    b.Property<string>("Hash");
-
-                    b.Property<int?>("MetadataID");
-
-                    b.Property<int?>("OnlineBeatmapSetID");
-
-                    b.Property<bool>("Protected");
-
-                    b.Property<int>("Status");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("DeletePending");
-
-                    b.HasIndex("Hash")
-                        .IsUnique();
-
-                    b.HasIndex("MetadataID");
-
-                    b.HasIndex("OnlineBeatmapSetID")
-                        .IsUnique();
-
-                    b.ToTable("BeatmapSetInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Configuration.DatabasedSetting", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<string>("Key")
-                        .HasColumnName("Key");
-
-                    b.Property<int?>("RulesetID");
-
-                    b.Property<int?>("SkinInfoID");
-
-                    b.Property<string>("StringValue")
-                        .HasColumnName("Value");
-
-                    b.Property<int?>("Variant");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("SkinInfoID");
-
-                    b.HasIndex("RulesetID", "Variant");
-
-                    b.ToTable("Settings");
-                });
-
-            modelBuilder.Entity("osu.Game.IO.FileInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<string>("Hash");
-
-                    b.Property<int>("ReferenceCount");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("Hash")
-                        .IsUnique();
-
-                    b.HasIndex("ReferenceCount");
-
-                    b.ToTable("FileInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Input.Bindings.DatabasedKeyBinding", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<int>("IntAction")
-                        .HasColumnName("Action");
-
-                    b.Property<string>("KeysString")
-                        .HasColumnName("Keys");
-
-                    b.Property<int?>("RulesetID");
-
-                    b.Property<int?>("Variant");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("IntAction");
-
-                    b.HasIndex("RulesetID", "Variant");
-
-                    b.ToTable("KeyBinding");
-                });
-
-            modelBuilder.Entity("osu.Game.Rulesets.RulesetInfo", b =>
-                {
-                    b.Property<int?>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<bool>("Available");
-
-                    b.Property<string>("InstantiationInfo");
-
-                    b.Property<string>("Name");
-
-                    b.Property<string>("ShortName");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("Available");
-
-                    b.HasIndex("ShortName")
-                        .IsUnique();
-
-                    b.ToTable("RulesetInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Scoring.ScoreFileInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<int>("FileInfoID");
-
-                    b.Property<string>("Filename")
-                        .IsRequired();
-
-                    b.Property<int?>("ScoreInfoID");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("FileInfoID");
-
-                    b.HasIndex("ScoreInfoID");
-
-                    b.ToTable("ScoreFileInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Scoring.ScoreInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<double>("Accuracy")
-                        .HasColumnType("DECIMAL(1,4)");
-
-                    b.Property<int>("BeatmapInfoID");
-
-                    b.Property<int>("Combo");
-
-                    b.Property<DateTimeOffset>("Date");
-
-                    b.Property<bool>("DeletePending");
-
-                    b.Property<string>("Hash");
-
-                    b.Property<int>("MaxCombo");
-
-                    b.Property<string>("ModsJson")
-                        .HasColumnName("Mods");
-
-                    b.Property<long?>("OnlineScoreID");
-
-                    b.Property<double?>("PP");
-
-                    b.Property<int>("Rank");
-
-                    b.Property<int>("RulesetID");
-
-                    b.Property<string>("StatisticsJson")
-                        .HasColumnName("Statistics");
-
-                    b.Property<long>("TotalScore");
-
-                    b.Property<int?>("UserID")
-                        .HasColumnName("UserID");
-
-                    b.Property<string>("UserString")
-                        .HasColumnName("User");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("BeatmapInfoID");
-
-                    b.HasIndex("OnlineScoreID")
-                        .IsUnique();
-
-                    b.HasIndex("RulesetID");
-
-                    b.ToTable("ScoreInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<int>("FileInfoID");
-
-                    b.Property<string>("Filename")
-                        .IsRequired();
-
-                    b.Property<int>("SkinInfoID");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("FileInfoID");
-
-                    b.HasIndex("SkinInfoID");
-
-                    b.ToTable("SkinFileInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Skinning.SkinInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<string>("Creator");
-
-                    b.Property<bool>("DeletePending");
-
-                    b.Property<string>("Hash");
-
-                    b.Property<string>("InstantiationInfo");
-
-                    b.Property<string>("Name");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("DeletePending");
-
-                    b.HasIndex("Hash")
-                        .IsUnique();
-
-                    b.ToTable("SkinInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b =>
-                {
-                    b.HasOne("osu.Game.Beatmaps.BeatmapDifficulty", "BaseDifficulty")
-                        .WithMany()
-                        .HasForeignKey("BaseDifficultyID")
-                        .OnDelete(DeleteBehavior.Cascade);
-
-                    b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo", "BeatmapSet")
-                        .WithMany("Beatmaps")
-                        .HasForeignKey("BeatmapSetInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-
-                    b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata")
-                        .WithMany("Beatmaps")
-                        .HasForeignKey("MetadataID");
-
-                    b.HasOne("osu.Game.Rulesets.RulesetInfo", "Ruleset")
-                        .WithMany()
-                        .HasForeignKey("RulesetID")
-                        .OnDelete(DeleteBehavior.Cascade);
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b =>
-                {
-                    b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo")
-                        .WithMany("Files")
-                        .HasForeignKey("BeatmapSetInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-
-                    b.HasOne("osu.Game.IO.FileInfo", "FileInfo")
-                        .WithMany()
-                        .HasForeignKey("FileInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b =>
-                {
-                    b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata")
-                        .WithMany("BeatmapSets")
-                        .HasForeignKey("MetadataID");
-                });
-
-            modelBuilder.Entity("osu.Game.Configuration.DatabasedSetting", b =>
-                {
-                    b.HasOne("osu.Game.Skinning.SkinInfo")
-                        .WithMany("Settings")
-                        .HasForeignKey("SkinInfoID");
-                });
-
-            modelBuilder.Entity("osu.Game.Scoring.ScoreFileInfo", b =>
-                {
-                    b.HasOne("osu.Game.IO.FileInfo", "FileInfo")
-                        .WithMany()
-                        .HasForeignKey("FileInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-
-                    b.HasOne("osu.Game.Scoring.ScoreInfo")
-                        .WithMany("Files")
-                        .HasForeignKey("ScoreInfoID");
-                });
-
-            modelBuilder.Entity("osu.Game.Scoring.ScoreInfo", b =>
-                {
-                    b.HasOne("osu.Game.Beatmaps.BeatmapInfo", "Beatmap")
-                        .WithMany("Scores")
-                        .HasForeignKey("BeatmapInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-
-                    b.HasOne("osu.Game.Rulesets.RulesetInfo", "Ruleset")
-                        .WithMany()
-                        .HasForeignKey("RulesetID")
-                        .OnDelete(DeleteBehavior.Cascade);
-                });
-
-            modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b =>
-                {
-                    b.HasOne("osu.Game.IO.FileInfo", "FileInfo")
-                        .WithMany()
-                        .HasForeignKey("FileInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-
-                    b.HasOne("osu.Game.Skinning.SkinInfo")
-                        .WithMany("Files")
-                        .HasForeignKey("SkinInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-                });
-#pragma warning restore 612, 618
-        }
-    }
-}
diff --git a/osu.Game/Migrations/20210511060743_AddSkinInstantiationInfo.cs b/osu.Game/Migrations/20210511060743_AddSkinInstantiationInfo.cs
deleted file mode 100644
index 1d5b0769a4..0000000000
--- a/osu.Game/Migrations/20210511060743_AddSkinInstantiationInfo.cs
+++ /dev/null
@@ -1,22 +0,0 @@
-using Microsoft.EntityFrameworkCore.Migrations;
-
-namespace osu.Game.Migrations
-{
-    public partial class AddSkinInstantiationInfo : Migration
-    {
-        protected override void Up(MigrationBuilder migrationBuilder)
-        {
-            migrationBuilder.AddColumn<string>(
-                name: "InstantiationInfo",
-                table: "SkinInfo",
-                nullable: true);
-        }
-
-        protected override void Down(MigrationBuilder migrationBuilder)
-        {
-            migrationBuilder.DropColumn(
-                name: "InstantiationInfo",
-                table: "SkinInfo");
-        }
-    }
-}
diff --git a/osu.Game/Migrations/20210514062639_AddAuthorIdToBeatmapMetadata.Designer.cs b/osu.Game/Migrations/20210514062639_AddAuthorIdToBeatmapMetadata.Designer.cs
deleted file mode 100644
index 89bab3a0fa..0000000000
--- a/osu.Game/Migrations/20210514062639_AddAuthorIdToBeatmapMetadata.Designer.cs
+++ /dev/null
@@ -1,511 +0,0 @@
-// <auto-generated />
-using System;
-using Microsoft.EntityFrameworkCore;
-using Microsoft.EntityFrameworkCore.Infrastructure;
-using Microsoft.EntityFrameworkCore.Migrations;
-using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
-using osu.Game.Database;
-
-namespace osu.Game.Migrations
-{
-    [DbContext(typeof(OsuDbContext))]
-    [Migration("20210514062639_AddAuthorIdToBeatmapMetadata")]
-    partial class AddAuthorIdToBeatmapMetadata
-    {
-        protected override void BuildTargetModel(ModelBuilder modelBuilder)
-        {
-#pragma warning disable 612, 618
-            modelBuilder
-                .HasAnnotation("ProductVersion", "2.2.6-servicing-10079");
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapDifficulty", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<float>("ApproachRate");
-
-                    b.Property<float>("CircleSize");
-
-                    b.Property<float>("DrainRate");
-
-                    b.Property<float>("OverallDifficulty");
-
-                    b.Property<double>("SliderMultiplier");
-
-                    b.Property<double>("SliderTickRate");
-
-                    b.HasKey("ID");
-
-                    b.ToTable("BeatmapDifficulty");
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<double>("AudioLeadIn");
-
-                    b.Property<double>("BPM");
-
-                    b.Property<int>("BaseDifficultyID");
-
-                    b.Property<int>("BeatDivisor");
-
-                    b.Property<int>("BeatmapSetInfoID");
-
-                    b.Property<bool>("Countdown");
-
-                    b.Property<double>("DistanceSpacing");
-
-                    b.Property<bool>("EpilepsyWarning");
-
-                    b.Property<int>("GridSize");
-
-                    b.Property<string>("Hash");
-
-                    b.Property<bool>("Hidden");
-
-                    b.Property<double>("Length");
-
-                    b.Property<bool>("LetterboxInBreaks");
-
-                    b.Property<string>("MD5Hash");
-
-                    b.Property<int?>("MetadataID");
-
-                    b.Property<int?>("OnlineBeatmapID");
-
-                    b.Property<string>("Path");
-
-                    b.Property<int>("RulesetID");
-
-                    b.Property<bool>("SpecialStyle");
-
-                    b.Property<float>("StackLeniency");
-
-                    b.Property<double>("StarDifficulty");
-
-                    b.Property<int>("Status");
-
-                    b.Property<string>("StoredBookmarks");
-
-                    b.Property<double>("TimelineZoom");
-
-                    b.Property<string>("Version");
-
-                    b.Property<bool>("WidescreenStoryboard");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("BaseDifficultyID");
-
-                    b.HasIndex("BeatmapSetInfoID");
-
-                    b.HasIndex("Hash");
-
-                    b.HasIndex("MD5Hash");
-
-                    b.HasIndex("MetadataID");
-
-                    b.HasIndex("OnlineBeatmapID")
-                        .IsUnique();
-
-                    b.HasIndex("RulesetID");
-
-                    b.ToTable("BeatmapInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapMetadata", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<string>("Artist");
-
-                    b.Property<string>("ArtistUnicode");
-
-                    b.Property<string>("AudioFile");
-
-                    b.Property<int>("AuthorID")
-                        .HasColumnName("AuthorID");
-
-                    b.Property<string>("AuthorString")
-                        .HasColumnName("Author");
-
-                    b.Property<string>("BackgroundFile");
-
-                    b.Property<int>("PreviewTime");
-
-                    b.Property<string>("Source");
-
-                    b.Property<string>("Tags");
-
-                    b.Property<string>("Title");
-
-                    b.Property<string>("TitleUnicode");
-
-                    b.HasKey("ID");
-
-                    b.ToTable("BeatmapMetadata");
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<int>("BeatmapSetInfoID");
-
-                    b.Property<int>("FileInfoID");
-
-                    b.Property<string>("Filename")
-                        .IsRequired();
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("BeatmapSetInfoID");
-
-                    b.HasIndex("FileInfoID");
-
-                    b.ToTable("BeatmapSetFileInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<DateTimeOffset>("DateAdded");
-
-                    b.Property<bool>("DeletePending");
-
-                    b.Property<string>("Hash");
-
-                    b.Property<int?>("MetadataID");
-
-                    b.Property<int?>("OnlineBeatmapSetID");
-
-                    b.Property<bool>("Protected");
-
-                    b.Property<int>("Status");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("DeletePending");
-
-                    b.HasIndex("Hash")
-                        .IsUnique();
-
-                    b.HasIndex("MetadataID");
-
-                    b.HasIndex("OnlineBeatmapSetID")
-                        .IsUnique();
-
-                    b.ToTable("BeatmapSetInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Configuration.DatabasedSetting", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<string>("Key")
-                        .HasColumnName("Key");
-
-                    b.Property<int?>("RulesetID");
-
-                    b.Property<int?>("SkinInfoID");
-
-                    b.Property<string>("StringValue")
-                        .HasColumnName("Value");
-
-                    b.Property<int?>("Variant");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("SkinInfoID");
-
-                    b.HasIndex("RulesetID", "Variant");
-
-                    b.ToTable("Settings");
-                });
-
-            modelBuilder.Entity("osu.Game.IO.FileInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<string>("Hash");
-
-                    b.Property<int>("ReferenceCount");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("Hash")
-                        .IsUnique();
-
-                    b.HasIndex("ReferenceCount");
-
-                    b.ToTable("FileInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Input.Bindings.DatabasedKeyBinding", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<int>("IntAction")
-                        .HasColumnName("Action");
-
-                    b.Property<string>("KeysString")
-                        .HasColumnName("Keys");
-
-                    b.Property<int?>("RulesetID");
-
-                    b.Property<int?>("Variant");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("IntAction");
-
-                    b.HasIndex("RulesetID", "Variant");
-
-                    b.ToTable("KeyBinding");
-                });
-
-            modelBuilder.Entity("osu.Game.Rulesets.RulesetInfo", b =>
-                {
-                    b.Property<int?>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<bool>("Available");
-
-                    b.Property<string>("InstantiationInfo");
-
-                    b.Property<string>("Name");
-
-                    b.Property<string>("ShortName");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("Available");
-
-                    b.HasIndex("ShortName")
-                        .IsUnique();
-
-                    b.ToTable("RulesetInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Scoring.ScoreFileInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<int>("FileInfoID");
-
-                    b.Property<string>("Filename")
-                        .IsRequired();
-
-                    b.Property<int?>("ScoreInfoID");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("FileInfoID");
-
-                    b.HasIndex("ScoreInfoID");
-
-                    b.ToTable("ScoreFileInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Scoring.ScoreInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<double>("Accuracy")
-                        .HasColumnType("DECIMAL(1,4)");
-
-                    b.Property<int>("BeatmapInfoID");
-
-                    b.Property<int>("Combo");
-
-                    b.Property<DateTimeOffset>("Date");
-
-                    b.Property<bool>("DeletePending");
-
-                    b.Property<string>("Hash");
-
-                    b.Property<int>("MaxCombo");
-
-                    b.Property<string>("ModsJson")
-                        .HasColumnName("Mods");
-
-                    b.Property<long?>("OnlineScoreID");
-
-                    b.Property<double?>("PP");
-
-                    b.Property<int>("Rank");
-
-                    b.Property<int>("RulesetID");
-
-                    b.Property<string>("StatisticsJson")
-                        .HasColumnName("Statistics");
-
-                    b.Property<long>("TotalScore");
-
-                    b.Property<int?>("UserID")
-                        .HasColumnName("UserID");
-
-                    b.Property<string>("UserString")
-                        .HasColumnName("User");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("BeatmapInfoID");
-
-                    b.HasIndex("OnlineScoreID")
-                        .IsUnique();
-
-                    b.HasIndex("RulesetID");
-
-                    b.ToTable("ScoreInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<int>("FileInfoID");
-
-                    b.Property<string>("Filename")
-                        .IsRequired();
-
-                    b.Property<int>("SkinInfoID");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("FileInfoID");
-
-                    b.HasIndex("SkinInfoID");
-
-                    b.ToTable("SkinFileInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Skinning.SkinInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<string>("Creator");
-
-                    b.Property<bool>("DeletePending");
-
-                    b.Property<string>("Hash");
-
-                    b.Property<string>("InstantiationInfo");
-
-                    b.Property<string>("Name");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("DeletePending");
-
-                    b.HasIndex("Hash")
-                        .IsUnique();
-
-                    b.ToTable("SkinInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b =>
-                {
-                    b.HasOne("osu.Game.Beatmaps.BeatmapDifficulty", "BaseDifficulty")
-                        .WithMany()
-                        .HasForeignKey("BaseDifficultyID")
-                        .OnDelete(DeleteBehavior.Cascade);
-
-                    b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo", "BeatmapSet")
-                        .WithMany("Beatmaps")
-                        .HasForeignKey("BeatmapSetInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-
-                    b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata")
-                        .WithMany("Beatmaps")
-                        .HasForeignKey("MetadataID");
-
-                    b.HasOne("osu.Game.Rulesets.RulesetInfo", "Ruleset")
-                        .WithMany()
-                        .HasForeignKey("RulesetID")
-                        .OnDelete(DeleteBehavior.Cascade);
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b =>
-                {
-                    b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo")
-                        .WithMany("Files")
-                        .HasForeignKey("BeatmapSetInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-
-                    b.HasOne("osu.Game.IO.FileInfo", "FileInfo")
-                        .WithMany()
-                        .HasForeignKey("FileInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b =>
-                {
-                    b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata")
-                        .WithMany("BeatmapSets")
-                        .HasForeignKey("MetadataID");
-                });
-
-            modelBuilder.Entity("osu.Game.Configuration.DatabasedSetting", b =>
-                {
-                    b.HasOne("osu.Game.Skinning.SkinInfo")
-                        .WithMany("Settings")
-                        .HasForeignKey("SkinInfoID");
-                });
-
-            modelBuilder.Entity("osu.Game.Scoring.ScoreFileInfo", b =>
-                {
-                    b.HasOne("osu.Game.IO.FileInfo", "FileInfo")
-                        .WithMany()
-                        .HasForeignKey("FileInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-
-                    b.HasOne("osu.Game.Scoring.ScoreInfo")
-                        .WithMany("Files")
-                        .HasForeignKey("ScoreInfoID");
-                });
-
-            modelBuilder.Entity("osu.Game.Scoring.ScoreInfo", b =>
-                {
-                    b.HasOne("osu.Game.Beatmaps.BeatmapInfo", "Beatmap")
-                        .WithMany("Scores")
-                        .HasForeignKey("BeatmapInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-
-                    b.HasOne("osu.Game.Rulesets.RulesetInfo", "Ruleset")
-                        .WithMany()
-                        .HasForeignKey("RulesetID")
-                        .OnDelete(DeleteBehavior.Cascade);
-                });
-
-            modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b =>
-                {
-                    b.HasOne("osu.Game.IO.FileInfo", "FileInfo")
-                        .WithMany()
-                        .HasForeignKey("FileInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-
-                    b.HasOne("osu.Game.Skinning.SkinInfo")
-                        .WithMany("Files")
-                        .HasForeignKey("SkinInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-                });
-#pragma warning restore 612, 618
-        }
-    }
-}
diff --git a/osu.Game/Migrations/20210514062639_AddAuthorIdToBeatmapMetadata.cs b/osu.Game/Migrations/20210514062639_AddAuthorIdToBeatmapMetadata.cs
deleted file mode 100644
index 98fe9b5e13..0000000000
--- a/osu.Game/Migrations/20210514062639_AddAuthorIdToBeatmapMetadata.cs
+++ /dev/null
@@ -1,23 +0,0 @@
-using Microsoft.EntityFrameworkCore.Migrations;
-
-namespace osu.Game.Migrations
-{
-    public partial class AddAuthorIdToBeatmapMetadata : Migration
-    {
-        protected override void Up(MigrationBuilder migrationBuilder)
-        {
-            migrationBuilder.AddColumn<int>(
-                name: "AuthorID",
-                table: "BeatmapMetadata",
-                nullable: false,
-                defaultValue: 0);
-        }
-
-        protected override void Down(MigrationBuilder migrationBuilder)
-        {
-            migrationBuilder.DropColumn(
-                name: "AuthorID",
-                table: "BeatmapMetadata");
-        }
-    }
-}
diff --git a/osu.Game/Migrations/20210824185035_AddCountdownSettings.Designer.cs b/osu.Game/Migrations/20210824185035_AddCountdownSettings.Designer.cs
deleted file mode 100644
index afeb42130d..0000000000
--- a/osu.Game/Migrations/20210824185035_AddCountdownSettings.Designer.cs
+++ /dev/null
@@ -1,513 +0,0 @@
-// <auto-generated />
-using System;
-using Microsoft.EntityFrameworkCore;
-using Microsoft.EntityFrameworkCore.Infrastructure;
-using Microsoft.EntityFrameworkCore.Migrations;
-using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
-using osu.Game.Database;
-
-namespace osu.Game.Migrations
-{
-    [DbContext(typeof(OsuDbContext))]
-    [Migration("20210824185035_AddCountdownSettings")]
-    partial class AddCountdownSettings
-    {
-        protected override void BuildTargetModel(ModelBuilder modelBuilder)
-        {
-#pragma warning disable 612, 618
-            modelBuilder
-                .HasAnnotation("ProductVersion", "2.2.6-servicing-10079");
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapDifficulty", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<float>("ApproachRate");
-
-                    b.Property<float>("CircleSize");
-
-                    b.Property<float>("DrainRate");
-
-                    b.Property<float>("OverallDifficulty");
-
-                    b.Property<double>("SliderMultiplier");
-
-                    b.Property<double>("SliderTickRate");
-
-                    b.HasKey("ID");
-
-                    b.ToTable("BeatmapDifficulty");
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<double>("AudioLeadIn");
-
-                    b.Property<double>("BPM");
-
-                    b.Property<int>("BaseDifficultyID");
-
-                    b.Property<int>("BeatDivisor");
-
-                    b.Property<int>("BeatmapSetInfoID");
-
-                    b.Property<int>("Countdown");
-
-                    b.Property<int>("CountdownOffset");
-
-                    b.Property<double>("DistanceSpacing");
-
-                    b.Property<bool>("EpilepsyWarning");
-
-                    b.Property<int>("GridSize");
-
-                    b.Property<string>("Hash");
-
-                    b.Property<bool>("Hidden");
-
-                    b.Property<double>("Length");
-
-                    b.Property<bool>("LetterboxInBreaks");
-
-                    b.Property<string>("MD5Hash");
-
-                    b.Property<int?>("MetadataID");
-
-                    b.Property<int?>("OnlineBeatmapID");
-
-                    b.Property<string>("Path");
-
-                    b.Property<int>("RulesetID");
-
-                    b.Property<bool>("SpecialStyle");
-
-                    b.Property<float>("StackLeniency");
-
-                    b.Property<double>("StarDifficulty");
-
-                    b.Property<int>("Status");
-
-                    b.Property<string>("StoredBookmarks");
-
-                    b.Property<double>("TimelineZoom");
-
-                    b.Property<string>("Version");
-
-                    b.Property<bool>("WidescreenStoryboard");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("BaseDifficultyID");
-
-                    b.HasIndex("BeatmapSetInfoID");
-
-                    b.HasIndex("Hash");
-
-                    b.HasIndex("MD5Hash");
-
-                    b.HasIndex("MetadataID");
-
-                    b.HasIndex("OnlineBeatmapID")
-                        .IsUnique();
-
-                    b.HasIndex("RulesetID");
-
-                    b.ToTable("BeatmapInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapMetadata", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<string>("Artist");
-
-                    b.Property<string>("ArtistUnicode");
-
-                    b.Property<string>("AudioFile");
-
-                    b.Property<int>("AuthorID")
-                        .HasColumnName("AuthorID");
-
-                    b.Property<string>("AuthorString")
-                        .HasColumnName("Author");
-
-                    b.Property<string>("BackgroundFile");
-
-                    b.Property<int>("PreviewTime");
-
-                    b.Property<string>("Source");
-
-                    b.Property<string>("Tags");
-
-                    b.Property<string>("Title");
-
-                    b.Property<string>("TitleUnicode");
-
-                    b.HasKey("ID");
-
-                    b.ToTable("BeatmapMetadata");
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<int>("BeatmapSetInfoID");
-
-                    b.Property<int>("FileInfoID");
-
-                    b.Property<string>("Filename")
-                        .IsRequired();
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("BeatmapSetInfoID");
-
-                    b.HasIndex("FileInfoID");
-
-                    b.ToTable("BeatmapSetFileInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<DateTimeOffset>("DateAdded");
-
-                    b.Property<bool>("DeletePending");
-
-                    b.Property<string>("Hash");
-
-                    b.Property<int?>("MetadataID");
-
-                    b.Property<int?>("OnlineBeatmapSetID");
-
-                    b.Property<bool>("Protected");
-
-                    b.Property<int>("Status");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("DeletePending");
-
-                    b.HasIndex("Hash")
-                        .IsUnique();
-
-                    b.HasIndex("MetadataID");
-
-                    b.HasIndex("OnlineBeatmapSetID")
-                        .IsUnique();
-
-                    b.ToTable("BeatmapSetInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Configuration.DatabasedSetting", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<string>("Key")
-                        .HasColumnName("Key");
-
-                    b.Property<int?>("RulesetID");
-
-                    b.Property<int?>("SkinInfoID");
-
-                    b.Property<string>("StringValue")
-                        .HasColumnName("Value");
-
-                    b.Property<int?>("Variant");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("SkinInfoID");
-
-                    b.HasIndex("RulesetID", "Variant");
-
-                    b.ToTable("Settings");
-                });
-
-            modelBuilder.Entity("osu.Game.IO.FileInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<string>("Hash");
-
-                    b.Property<int>("ReferenceCount");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("Hash")
-                        .IsUnique();
-
-                    b.HasIndex("ReferenceCount");
-
-                    b.ToTable("FileInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Input.Bindings.DatabasedKeyBinding", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<int>("IntAction")
-                        .HasColumnName("Action");
-
-                    b.Property<string>("KeysString")
-                        .HasColumnName("Keys");
-
-                    b.Property<int?>("RulesetID");
-
-                    b.Property<int?>("Variant");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("IntAction");
-
-                    b.HasIndex("RulesetID", "Variant");
-
-                    b.ToTable("KeyBinding");
-                });
-
-            modelBuilder.Entity("osu.Game.Rulesets.RulesetInfo", b =>
-                {
-                    b.Property<int?>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<bool>("Available");
-
-                    b.Property<string>("InstantiationInfo");
-
-                    b.Property<string>("Name");
-
-                    b.Property<string>("ShortName");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("Available");
-
-                    b.HasIndex("ShortName")
-                        .IsUnique();
-
-                    b.ToTable("RulesetInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Scoring.ScoreFileInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<int>("FileInfoID");
-
-                    b.Property<string>("Filename")
-                        .IsRequired();
-
-                    b.Property<int?>("ScoreInfoID");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("FileInfoID");
-
-                    b.HasIndex("ScoreInfoID");
-
-                    b.ToTable("ScoreFileInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Scoring.ScoreInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<double>("Accuracy")
-                        .HasColumnType("DECIMAL(1,4)");
-
-                    b.Property<int>("BeatmapInfoID");
-
-                    b.Property<int>("Combo");
-
-                    b.Property<DateTimeOffset>("Date");
-
-                    b.Property<bool>("DeletePending");
-
-                    b.Property<string>("Hash");
-
-                    b.Property<int>("MaxCombo");
-
-                    b.Property<string>("ModsJson")
-                        .HasColumnName("Mods");
-
-                    b.Property<long?>("OnlineScoreID");
-
-                    b.Property<double?>("PP");
-
-                    b.Property<int>("Rank");
-
-                    b.Property<int>("RulesetID");
-
-                    b.Property<string>("StatisticsJson")
-                        .HasColumnName("Statistics");
-
-                    b.Property<long>("TotalScore");
-
-                    b.Property<int?>("UserID")
-                        .HasColumnName("UserID");
-
-                    b.Property<string>("UserString")
-                        .HasColumnName("User");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("BeatmapInfoID");
-
-                    b.HasIndex("OnlineScoreID")
-                        .IsUnique();
-
-                    b.HasIndex("RulesetID");
-
-                    b.ToTable("ScoreInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<int>("FileInfoID");
-
-                    b.Property<string>("Filename")
-                        .IsRequired();
-
-                    b.Property<int>("SkinInfoID");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("FileInfoID");
-
-                    b.HasIndex("SkinInfoID");
-
-                    b.ToTable("SkinFileInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Skinning.SkinInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<string>("Creator");
-
-                    b.Property<bool>("DeletePending");
-
-                    b.Property<string>("Hash");
-
-                    b.Property<string>("InstantiationInfo");
-
-                    b.Property<string>("Name");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("DeletePending");
-
-                    b.HasIndex("Hash")
-                        .IsUnique();
-
-                    b.ToTable("SkinInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b =>
-                {
-                    b.HasOne("osu.Game.Beatmaps.BeatmapDifficulty", "BaseDifficulty")
-                        .WithMany()
-                        .HasForeignKey("BaseDifficultyID")
-                        .OnDelete(DeleteBehavior.Cascade);
-
-                    b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo", "BeatmapSet")
-                        .WithMany("Beatmaps")
-                        .HasForeignKey("BeatmapSetInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-
-                    b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata")
-                        .WithMany("Beatmaps")
-                        .HasForeignKey("MetadataID");
-
-                    b.HasOne("osu.Game.Rulesets.RulesetInfo", "Ruleset")
-                        .WithMany()
-                        .HasForeignKey("RulesetID")
-                        .OnDelete(DeleteBehavior.Cascade);
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b =>
-                {
-                    b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo")
-                        .WithMany("Files")
-                        .HasForeignKey("BeatmapSetInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-
-                    b.HasOne("osu.Game.IO.FileInfo", "FileInfo")
-                        .WithMany()
-                        .HasForeignKey("FileInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b =>
-                {
-                    b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata")
-                        .WithMany("BeatmapSets")
-                        .HasForeignKey("MetadataID");
-                });
-
-            modelBuilder.Entity("osu.Game.Configuration.DatabasedSetting", b =>
-                {
-                    b.HasOne("osu.Game.Skinning.SkinInfo")
-                        .WithMany("Settings")
-                        .HasForeignKey("SkinInfoID");
-                });
-
-            modelBuilder.Entity("osu.Game.Scoring.ScoreFileInfo", b =>
-                {
-                    b.HasOne("osu.Game.IO.FileInfo", "FileInfo")
-                        .WithMany()
-                        .HasForeignKey("FileInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-
-                    b.HasOne("osu.Game.Scoring.ScoreInfo")
-                        .WithMany("Files")
-                        .HasForeignKey("ScoreInfoID");
-                });
-
-            modelBuilder.Entity("osu.Game.Scoring.ScoreInfo", b =>
-                {
-                    b.HasOne("osu.Game.Beatmaps.BeatmapInfo", "Beatmap")
-                        .WithMany("Scores")
-                        .HasForeignKey("BeatmapInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-
-                    b.HasOne("osu.Game.Rulesets.RulesetInfo", "Ruleset")
-                        .WithMany()
-                        .HasForeignKey("RulesetID")
-                        .OnDelete(DeleteBehavior.Cascade);
-                });
-
-            modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b =>
-                {
-                    b.HasOne("osu.Game.IO.FileInfo", "FileInfo")
-                        .WithMany()
-                        .HasForeignKey("FileInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-
-                    b.HasOne("osu.Game.Skinning.SkinInfo")
-                        .WithMany("Files")
-                        .HasForeignKey("SkinInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-                });
-#pragma warning restore 612, 618
-        }
-    }
-}
diff --git a/osu.Game/Migrations/20210824185035_AddCountdownSettings.cs b/osu.Game/Migrations/20210824185035_AddCountdownSettings.cs
deleted file mode 100644
index 564f5f4520..0000000000
--- a/osu.Game/Migrations/20210824185035_AddCountdownSettings.cs
+++ /dev/null
@@ -1,23 +0,0 @@
-using Microsoft.EntityFrameworkCore.Migrations;
-
-namespace osu.Game.Migrations
-{
-    public partial class AddCountdownSettings : Migration
-    {
-        protected override void Up(MigrationBuilder migrationBuilder)
-        {
-            migrationBuilder.AddColumn<int>(
-                name: "CountdownOffset",
-                table: "BeatmapInfo",
-                nullable: false,
-                defaultValue: 0);
-        }
-
-        protected override void Down(MigrationBuilder migrationBuilder)
-        {
-            migrationBuilder.DropColumn(
-                name: "CountdownOffset",
-                table: "BeatmapInfo");
-        }
-    }
-}
diff --git a/osu.Game/Migrations/20210912144011_AddSamplesMatchPlaybackRate.Designer.cs b/osu.Game/Migrations/20210912144011_AddSamplesMatchPlaybackRate.Designer.cs
deleted file mode 100644
index 6e53d7fae0..0000000000
--- a/osu.Game/Migrations/20210912144011_AddSamplesMatchPlaybackRate.Designer.cs
+++ /dev/null
@@ -1,515 +0,0 @@
-// <auto-generated />
-using System;
-using Microsoft.EntityFrameworkCore;
-using Microsoft.EntityFrameworkCore.Infrastructure;
-using Microsoft.EntityFrameworkCore.Migrations;
-using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
-using osu.Game.Database;
-
-namespace osu.Game.Migrations
-{
-    [DbContext(typeof(OsuDbContext))]
-    [Migration("20210912144011_AddSamplesMatchPlaybackRate")]
-    partial class AddSamplesMatchPlaybackRate
-    {
-        protected override void BuildTargetModel(ModelBuilder modelBuilder)
-        {
-#pragma warning disable 612, 618
-            modelBuilder
-                .HasAnnotation("ProductVersion", "2.2.6-servicing-10079");
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapDifficulty", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<float>("ApproachRate");
-
-                    b.Property<float>("CircleSize");
-
-                    b.Property<float>("DrainRate");
-
-                    b.Property<float>("OverallDifficulty");
-
-                    b.Property<double>("SliderMultiplier");
-
-                    b.Property<double>("SliderTickRate");
-
-                    b.HasKey("ID");
-
-                    b.ToTable("BeatmapDifficulty");
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<double>("AudioLeadIn");
-
-                    b.Property<double>("BPM");
-
-                    b.Property<int>("BaseDifficultyID");
-
-                    b.Property<int>("BeatDivisor");
-
-                    b.Property<int>("BeatmapSetInfoID");
-
-                    b.Property<int>("Countdown");
-
-                    b.Property<int>("CountdownOffset");
-
-                    b.Property<double>("DistanceSpacing");
-
-                    b.Property<bool>("EpilepsyWarning");
-
-                    b.Property<int>("GridSize");
-
-                    b.Property<string>("Hash");
-
-                    b.Property<bool>("Hidden");
-
-                    b.Property<double>("Length");
-
-                    b.Property<bool>("LetterboxInBreaks");
-
-                    b.Property<string>("MD5Hash");
-
-                    b.Property<int?>("MetadataID");
-
-                    b.Property<int?>("OnlineBeatmapID");
-
-                    b.Property<string>("Path");
-
-                    b.Property<int>("RulesetID");
-
-                    b.Property<bool>("SamplesMatchPlaybackRate");
-
-                    b.Property<bool>("SpecialStyle");
-
-                    b.Property<float>("StackLeniency");
-
-                    b.Property<double>("StarDifficulty");
-
-                    b.Property<int>("Status");
-
-                    b.Property<string>("StoredBookmarks");
-
-                    b.Property<double>("TimelineZoom");
-
-                    b.Property<string>("Version");
-
-                    b.Property<bool>("WidescreenStoryboard");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("BaseDifficultyID");
-
-                    b.HasIndex("BeatmapSetInfoID");
-
-                    b.HasIndex("Hash");
-
-                    b.HasIndex("MD5Hash");
-
-                    b.HasIndex("MetadataID");
-
-                    b.HasIndex("OnlineBeatmapID")
-                        .IsUnique();
-
-                    b.HasIndex("RulesetID");
-
-                    b.ToTable("BeatmapInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapMetadata", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<string>("Artist");
-
-                    b.Property<string>("ArtistUnicode");
-
-                    b.Property<string>("AudioFile");
-
-                    b.Property<int>("AuthorID")
-                        .HasColumnName("AuthorID");
-
-                    b.Property<string>("AuthorString")
-                        .HasColumnName("Author");
-
-                    b.Property<string>("BackgroundFile");
-
-                    b.Property<int>("PreviewTime");
-
-                    b.Property<string>("Source");
-
-                    b.Property<string>("Tags");
-
-                    b.Property<string>("Title");
-
-                    b.Property<string>("TitleUnicode");
-
-                    b.HasKey("ID");
-
-                    b.ToTable("BeatmapMetadata");
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<int>("BeatmapSetInfoID");
-
-                    b.Property<int>("FileInfoID");
-
-                    b.Property<string>("Filename")
-                        .IsRequired();
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("BeatmapSetInfoID");
-
-                    b.HasIndex("FileInfoID");
-
-                    b.ToTable("BeatmapSetFileInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<DateTimeOffset>("DateAdded");
-
-                    b.Property<bool>("DeletePending");
-
-                    b.Property<string>("Hash");
-
-                    b.Property<int?>("MetadataID");
-
-                    b.Property<int?>("OnlineBeatmapSetID");
-
-                    b.Property<bool>("Protected");
-
-                    b.Property<int>("Status");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("DeletePending");
-
-                    b.HasIndex("Hash")
-                        .IsUnique();
-
-                    b.HasIndex("MetadataID");
-
-                    b.HasIndex("OnlineBeatmapSetID")
-                        .IsUnique();
-
-                    b.ToTable("BeatmapSetInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Configuration.DatabasedSetting", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<string>("Key")
-                        .HasColumnName("Key");
-
-                    b.Property<int?>("RulesetID");
-
-                    b.Property<int?>("SkinInfoID");
-
-                    b.Property<string>("StringValue")
-                        .HasColumnName("Value");
-
-                    b.Property<int?>("Variant");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("SkinInfoID");
-
-                    b.HasIndex("RulesetID", "Variant");
-
-                    b.ToTable("Settings");
-                });
-
-            modelBuilder.Entity("osu.Game.IO.FileInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<string>("Hash");
-
-                    b.Property<int>("ReferenceCount");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("Hash")
-                        .IsUnique();
-
-                    b.HasIndex("ReferenceCount");
-
-                    b.ToTable("FileInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Input.Bindings.DatabasedKeyBinding", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<int>("IntAction")
-                        .HasColumnName("Action");
-
-                    b.Property<string>("KeysString")
-                        .HasColumnName("Keys");
-
-                    b.Property<int?>("RulesetID");
-
-                    b.Property<int?>("Variant");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("IntAction");
-
-                    b.HasIndex("RulesetID", "Variant");
-
-                    b.ToTable("KeyBinding");
-                });
-
-            modelBuilder.Entity("osu.Game.Rulesets.RulesetInfo", b =>
-                {
-                    b.Property<int?>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<bool>("Available");
-
-                    b.Property<string>("InstantiationInfo");
-
-                    b.Property<string>("Name");
-
-                    b.Property<string>("ShortName");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("Available");
-
-                    b.HasIndex("ShortName")
-                        .IsUnique();
-
-                    b.ToTable("RulesetInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Scoring.ScoreFileInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<int>("FileInfoID");
-
-                    b.Property<string>("Filename")
-                        .IsRequired();
-
-                    b.Property<int?>("ScoreInfoID");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("FileInfoID");
-
-                    b.HasIndex("ScoreInfoID");
-
-                    b.ToTable("ScoreFileInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Scoring.ScoreInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<double>("Accuracy")
-                        .HasColumnType("DECIMAL(1,4)");
-
-                    b.Property<int>("BeatmapInfoID");
-
-                    b.Property<int>("Combo");
-
-                    b.Property<DateTimeOffset>("Date");
-
-                    b.Property<bool>("DeletePending");
-
-                    b.Property<string>("Hash");
-
-                    b.Property<int>("MaxCombo");
-
-                    b.Property<string>("ModsJson")
-                        .HasColumnName("Mods");
-
-                    b.Property<long?>("OnlineScoreID");
-
-                    b.Property<double?>("PP");
-
-                    b.Property<int>("Rank");
-
-                    b.Property<int>("RulesetID");
-
-                    b.Property<string>("StatisticsJson")
-                        .HasColumnName("Statistics");
-
-                    b.Property<long>("TotalScore");
-
-                    b.Property<int?>("UserID")
-                        .HasColumnName("UserID");
-
-                    b.Property<string>("UserString")
-                        .HasColumnName("User");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("BeatmapInfoID");
-
-                    b.HasIndex("OnlineScoreID")
-                        .IsUnique();
-
-                    b.HasIndex("RulesetID");
-
-                    b.ToTable("ScoreInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<int>("FileInfoID");
-
-                    b.Property<string>("Filename")
-                        .IsRequired();
-
-                    b.Property<int>("SkinInfoID");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("FileInfoID");
-
-                    b.HasIndex("SkinInfoID");
-
-                    b.ToTable("SkinFileInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Skinning.SkinInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<string>("Creator");
-
-                    b.Property<bool>("DeletePending");
-
-                    b.Property<string>("Hash");
-
-                    b.Property<string>("InstantiationInfo");
-
-                    b.Property<string>("Name");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("DeletePending");
-
-                    b.HasIndex("Hash")
-                        .IsUnique();
-
-                    b.ToTable("SkinInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b =>
-                {
-                    b.HasOne("osu.Game.Beatmaps.BeatmapDifficulty", "BaseDifficulty")
-                        .WithMany()
-                        .HasForeignKey("BaseDifficultyID")
-                        .OnDelete(DeleteBehavior.Cascade);
-
-                    b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo", "BeatmapSet")
-                        .WithMany("Beatmaps")
-                        .HasForeignKey("BeatmapSetInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-
-                    b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata")
-                        .WithMany("Beatmaps")
-                        .HasForeignKey("MetadataID");
-
-                    b.HasOne("osu.Game.Rulesets.RulesetInfo", "Ruleset")
-                        .WithMany()
-                        .HasForeignKey("RulesetID")
-                        .OnDelete(DeleteBehavior.Cascade);
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b =>
-                {
-                    b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo")
-                        .WithMany("Files")
-                        .HasForeignKey("BeatmapSetInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-
-                    b.HasOne("osu.Game.IO.FileInfo", "FileInfo")
-                        .WithMany()
-                        .HasForeignKey("FileInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b =>
-                {
-                    b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata")
-                        .WithMany("BeatmapSets")
-                        .HasForeignKey("MetadataID");
-                });
-
-            modelBuilder.Entity("osu.Game.Configuration.DatabasedSetting", b =>
-                {
-                    b.HasOne("osu.Game.Skinning.SkinInfo")
-                        .WithMany("Settings")
-                        .HasForeignKey("SkinInfoID");
-                });
-
-            modelBuilder.Entity("osu.Game.Scoring.ScoreFileInfo", b =>
-                {
-                    b.HasOne("osu.Game.IO.FileInfo", "FileInfo")
-                        .WithMany()
-                        .HasForeignKey("FileInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-
-                    b.HasOne("osu.Game.Scoring.ScoreInfo")
-                        .WithMany("Files")
-                        .HasForeignKey("ScoreInfoID");
-                });
-
-            modelBuilder.Entity("osu.Game.Scoring.ScoreInfo", b =>
-                {
-                    b.HasOne("osu.Game.Beatmaps.BeatmapInfo", "Beatmap")
-                        .WithMany("Scores")
-                        .HasForeignKey("BeatmapInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-
-                    b.HasOne("osu.Game.Rulesets.RulesetInfo", "Ruleset")
-                        .WithMany()
-                        .HasForeignKey("RulesetID")
-                        .OnDelete(DeleteBehavior.Cascade);
-                });
-
-            modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b =>
-                {
-                    b.HasOne("osu.Game.IO.FileInfo", "FileInfo")
-                        .WithMany()
-                        .HasForeignKey("FileInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-
-                    b.HasOne("osu.Game.Skinning.SkinInfo")
-                        .WithMany("Files")
-                        .HasForeignKey("SkinInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-                });
-#pragma warning restore 612, 618
-        }
-    }
-}
diff --git a/osu.Game/Migrations/20210912144011_AddSamplesMatchPlaybackRate.cs b/osu.Game/Migrations/20210912144011_AddSamplesMatchPlaybackRate.cs
deleted file mode 100644
index bf3f855d5f..0000000000
--- a/osu.Game/Migrations/20210912144011_AddSamplesMatchPlaybackRate.cs
+++ /dev/null
@@ -1,23 +0,0 @@
-using Microsoft.EntityFrameworkCore.Migrations;
-
-namespace osu.Game.Migrations
-{
-    public partial class AddSamplesMatchPlaybackRate : Migration
-    {
-        protected override void Up(MigrationBuilder migrationBuilder)
-        {
-            migrationBuilder.AddColumn<bool>(
-                name: "SamplesMatchPlaybackRate",
-                table: "BeatmapInfo",
-                nullable: false,
-                defaultValue: false);
-        }
-
-        protected override void Down(MigrationBuilder migrationBuilder)
-        {
-            migrationBuilder.DropColumn(
-                name: "SamplesMatchPlaybackRate",
-                table: "BeatmapInfo");
-        }
-    }
-}
diff --git a/osu.Game/Migrations/20211020081609_ResetSkinHashes.cs b/osu.Game/Migrations/20211020081609_ResetSkinHashes.cs
deleted file mode 100644
index 6d53c019ec..0000000000
--- a/osu.Game/Migrations/20211020081609_ResetSkinHashes.cs
+++ /dev/null
@@ -1,23 +0,0 @@
-// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using Microsoft.EntityFrameworkCore.Infrastructure;
-using Microsoft.EntityFrameworkCore.Migrations;
-using osu.Game.Database;
-
-namespace osu.Game.Migrations
-{
-    [DbContext(typeof(OsuDbContext))]
-    [Migration("20211020081609_ResetSkinHashes")]
-    public partial class ResetSkinHashes : Migration
-    {
-        protected override void Up(MigrationBuilder migrationBuilder)
-        {
-            migrationBuilder.Sql($"UPDATE SkinInfo SET Hash = null");
-        }
-
-        protected override void Down(MigrationBuilder migrationBuilder)
-        {
-        }
-    }
-}
diff --git a/osu.Game/Migrations/OsuDbContextModelSnapshot.cs b/osu.Game/Migrations/OsuDbContextModelSnapshot.cs
deleted file mode 100644
index 036c26cb0a..0000000000
--- a/osu.Game/Migrations/OsuDbContextModelSnapshot.cs
+++ /dev/null
@@ -1,513 +0,0 @@
-// <auto-generated />
-using System;
-using Microsoft.EntityFrameworkCore;
-using Microsoft.EntityFrameworkCore.Infrastructure;
-using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
-using osu.Game.Database;
-
-namespace osu.Game.Migrations
-{
-    [DbContext(typeof(OsuDbContext))]
-    partial class OsuDbContextModelSnapshot : ModelSnapshot
-    {
-        protected override void BuildModel(ModelBuilder modelBuilder)
-        {
-#pragma warning disable 612, 618
-            modelBuilder
-                .HasAnnotation("ProductVersion", "2.2.6-servicing-10079");
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapDifficulty", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<float>("ApproachRate");
-
-                    b.Property<float>("CircleSize");
-
-                    b.Property<float>("DrainRate");
-
-                    b.Property<float>("OverallDifficulty");
-
-                    b.Property<double>("SliderMultiplier");
-
-                    b.Property<double>("SliderTickRate");
-
-                    b.HasKey("ID");
-
-                    b.ToTable("BeatmapDifficulty");
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<double>("AudioLeadIn");
-
-                    b.Property<double>("BPM");
-
-                    b.Property<int>("BaseDifficultyID");
-
-                    b.Property<int>("BeatDivisor");
-
-                    b.Property<int>("BeatmapSetInfoID");
-
-                    b.Property<int>("Countdown");
-
-                    b.Property<int>("CountdownOffset");
-
-                    b.Property<double>("DistanceSpacing");
-
-                    b.Property<bool>("EpilepsyWarning");
-
-                    b.Property<int>("GridSize");
-
-                    b.Property<string>("Hash");
-
-                    b.Property<bool>("Hidden");
-
-                    b.Property<double>("Length");
-
-                    b.Property<bool>("LetterboxInBreaks");
-
-                    b.Property<string>("MD5Hash");
-
-                    b.Property<int?>("MetadataID");
-
-                    b.Property<int?>("OnlineBeatmapID");
-
-                    b.Property<string>("Path");
-
-                    b.Property<int>("RulesetID");
-
-                    b.Property<bool>("SamplesMatchPlaybackRate");
-
-                    b.Property<bool>("SpecialStyle");
-
-                    b.Property<float>("StackLeniency");
-
-                    b.Property<double>("StarDifficulty");
-
-                    b.Property<int>("Status");
-
-                    b.Property<string>("StoredBookmarks");
-
-                    b.Property<double>("TimelineZoom");
-
-                    b.Property<string>("Version");
-
-                    b.Property<bool>("WidescreenStoryboard");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("BaseDifficultyID");
-
-                    b.HasIndex("BeatmapSetInfoID");
-
-                    b.HasIndex("Hash");
-
-                    b.HasIndex("MD5Hash");
-
-                    b.HasIndex("MetadataID");
-
-                    b.HasIndex("OnlineBeatmapID")
-                        .IsUnique();
-
-                    b.HasIndex("RulesetID");
-
-                    b.ToTable("BeatmapInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapMetadata", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<string>("Artist");
-
-                    b.Property<string>("ArtistUnicode");
-
-                    b.Property<string>("AudioFile");
-
-                    b.Property<int>("AuthorID")
-                        .HasColumnName("AuthorID");
-
-                    b.Property<string>("AuthorString")
-                        .HasColumnName("Author");
-
-                    b.Property<string>("BackgroundFile");
-
-                    b.Property<int>("PreviewTime");
-
-                    b.Property<string>("Source");
-
-                    b.Property<string>("Tags");
-
-                    b.Property<string>("Title");
-
-                    b.Property<string>("TitleUnicode");
-
-                    b.HasKey("ID");
-
-                    b.ToTable("BeatmapMetadata");
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<int>("BeatmapSetInfoID");
-
-                    b.Property<int>("FileInfoID");
-
-                    b.Property<string>("Filename")
-                        .IsRequired();
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("BeatmapSetInfoID");
-
-                    b.HasIndex("FileInfoID");
-
-                    b.ToTable("BeatmapSetFileInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<DateTimeOffset>("DateAdded");
-
-                    b.Property<bool>("DeletePending");
-
-                    b.Property<string>("Hash");
-
-                    b.Property<int?>("MetadataID");
-
-                    b.Property<int?>("OnlineBeatmapSetID");
-
-                    b.Property<bool>("Protected");
-
-                    b.Property<int>("Status");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("DeletePending");
-
-                    b.HasIndex("Hash")
-                        .IsUnique();
-
-                    b.HasIndex("MetadataID");
-
-                    b.HasIndex("OnlineBeatmapSetID")
-                        .IsUnique();
-
-                    b.ToTable("BeatmapSetInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Configuration.DatabasedSetting", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<string>("Key")
-                        .HasColumnName("Key");
-
-                    b.Property<int?>("RulesetID");
-
-                    b.Property<int?>("SkinInfoID");
-
-                    b.Property<string>("StringValue")
-                        .HasColumnName("Value");
-
-                    b.Property<int?>("Variant");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("SkinInfoID");
-
-                    b.HasIndex("RulesetID", "Variant");
-
-                    b.ToTable("Settings");
-                });
-
-            modelBuilder.Entity("osu.Game.IO.FileInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<string>("Hash");
-
-                    b.Property<int>("ReferenceCount");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("Hash")
-                        .IsUnique();
-
-                    b.HasIndex("ReferenceCount");
-
-                    b.ToTable("FileInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Input.Bindings.DatabasedKeyBinding", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<int>("IntAction")
-                        .HasColumnName("Action");
-
-                    b.Property<string>("KeysString")
-                        .HasColumnName("Keys");
-
-                    b.Property<int?>("RulesetID");
-
-                    b.Property<int?>("Variant");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("IntAction");
-
-                    b.HasIndex("RulesetID", "Variant");
-
-                    b.ToTable("KeyBinding");
-                });
-
-            modelBuilder.Entity("osu.Game.Rulesets.RulesetInfo", b =>
-                {
-                    b.Property<int?>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<bool>("Available");
-
-                    b.Property<string>("InstantiationInfo");
-
-                    b.Property<string>("Name");
-
-                    b.Property<string>("ShortName");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("Available");
-
-                    b.HasIndex("ShortName")
-                        .IsUnique();
-
-                    b.ToTable("RulesetInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Scoring.ScoreFileInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<int>("FileInfoID");
-
-                    b.Property<string>("Filename")
-                        .IsRequired();
-
-                    b.Property<int?>("ScoreInfoID");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("FileInfoID");
-
-                    b.HasIndex("ScoreInfoID");
-
-                    b.ToTable("ScoreFileInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Scoring.ScoreInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<double>("Accuracy")
-                        .HasColumnType("DECIMAL(1,4)");
-
-                    b.Property<int>("BeatmapInfoID");
-
-                    b.Property<int>("Combo");
-
-                    b.Property<DateTimeOffset>("Date");
-
-                    b.Property<bool>("DeletePending");
-
-                    b.Property<string>("Hash");
-
-                    b.Property<int>("MaxCombo");
-
-                    b.Property<string>("ModsJson")
-                        .HasColumnName("Mods");
-
-                    b.Property<long?>("OnlineScoreID");
-
-                    b.Property<double?>("PP");
-
-                    b.Property<int>("Rank");
-
-                    b.Property<int>("RulesetID");
-
-                    b.Property<string>("StatisticsJson")
-                        .HasColumnName("Statistics");
-
-                    b.Property<long>("TotalScore");
-
-                    b.Property<int?>("UserID")
-                        .HasColumnName("UserID");
-
-                    b.Property<string>("UserString")
-                        .HasColumnName("User");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("BeatmapInfoID");
-
-                    b.HasIndex("OnlineScoreID")
-                        .IsUnique();
-
-                    b.HasIndex("RulesetID");
-
-                    b.ToTable("ScoreInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<int>("FileInfoID");
-
-                    b.Property<string>("Filename")
-                        .IsRequired();
-
-                    b.Property<int>("SkinInfoID");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("FileInfoID");
-
-                    b.HasIndex("SkinInfoID");
-
-                    b.ToTable("SkinFileInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Skinning.SkinInfo", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd();
-
-                    b.Property<string>("Creator");
-
-                    b.Property<bool>("DeletePending");
-
-                    b.Property<string>("Hash");
-
-                    b.Property<string>("InstantiationInfo");
-
-                    b.Property<string>("Name");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("DeletePending");
-
-                    b.HasIndex("Hash")
-                        .IsUnique();
-
-                    b.ToTable("SkinInfo");
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b =>
-                {
-                    b.HasOne("osu.Game.Beatmaps.BeatmapDifficulty", "BaseDifficulty")
-                        .WithMany()
-                        .HasForeignKey("BaseDifficultyID")
-                        .OnDelete(DeleteBehavior.Cascade);
-
-                    b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo", "BeatmapSet")
-                        .WithMany("Beatmaps")
-                        .HasForeignKey("BeatmapSetInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-
-                    b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata")
-                        .WithMany("Beatmaps")
-                        .HasForeignKey("MetadataID");
-
-                    b.HasOne("osu.Game.Rulesets.RulesetInfo", "Ruleset")
-                        .WithMany()
-                        .HasForeignKey("RulesetID")
-                        .OnDelete(DeleteBehavior.Cascade);
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b =>
-                {
-                    b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo")
-                        .WithMany("Files")
-                        .HasForeignKey("BeatmapSetInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-
-                    b.HasOne("osu.Game.IO.FileInfo", "FileInfo")
-                        .WithMany()
-                        .HasForeignKey("FileInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-                });
-
-            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b =>
-                {
-                    b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata")
-                        .WithMany("BeatmapSets")
-                        .HasForeignKey("MetadataID");
-                });
-
-            modelBuilder.Entity("osu.Game.Configuration.DatabasedSetting", b =>
-                {
-                    b.HasOne("osu.Game.Skinning.SkinInfo")
-                        .WithMany("Settings")
-                        .HasForeignKey("SkinInfoID");
-                });
-
-            modelBuilder.Entity("osu.Game.Scoring.ScoreFileInfo", b =>
-                {
-                    b.HasOne("osu.Game.IO.FileInfo", "FileInfo")
-                        .WithMany()
-                        .HasForeignKey("FileInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-
-                    b.HasOne("osu.Game.Scoring.ScoreInfo")
-                        .WithMany("Files")
-                        .HasForeignKey("ScoreInfoID");
-                });
-
-            modelBuilder.Entity("osu.Game.Scoring.ScoreInfo", b =>
-                {
-                    b.HasOne("osu.Game.Beatmaps.BeatmapInfo", "Beatmap")
-                        .WithMany("Scores")
-                        .HasForeignKey("BeatmapInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-
-                    b.HasOne("osu.Game.Rulesets.RulesetInfo", "Ruleset")
-                        .WithMany()
-                        .HasForeignKey("RulesetID")
-                        .OnDelete(DeleteBehavior.Cascade);
-                });
-
-            modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b =>
-                {
-                    b.HasOne("osu.Game.IO.FileInfo", "FileInfo")
-                        .WithMany()
-                        .HasForeignKey("FileInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-
-                    b.HasOne("osu.Game.Skinning.SkinInfo")
-                        .WithMany("Files")
-                        .HasForeignKey("SkinInfoID")
-                        .OnDelete(DeleteBehavior.Cascade);
-                });
-#pragma warning restore 612, 618
-        }
-    }
-}
diff --git a/osu.Game/Models/RealmBeatmap.cs b/osu.Game/Models/RealmBeatmap.cs
deleted file mode 100644
index 8e132687f7..0000000000
--- a/osu.Game/Models/RealmBeatmap.cs
+++ /dev/null
@@ -1,132 +0,0 @@
-// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using System;
-using System.Linq;
-using JetBrains.Annotations;
-using Newtonsoft.Json;
-using osu.Framework.Testing;
-using osu.Game.Beatmaps;
-using osu.Game.Database;
-using osu.Game.Rulesets;
-using Realms;
-
-#nullable enable
-
-namespace osu.Game.Models
-{
-    /// <summary>
-    /// A single beatmap difficulty.
-    /// </summary>
-    [ExcludeFromDynamicCompile]
-    [Serializable]
-    [MapTo("Beatmap")]
-    public class RealmBeatmap : RealmObject, IHasGuidPrimaryKey, IBeatmapInfo, IEquatable<RealmBeatmap>
-    {
-        [PrimaryKey]
-        public Guid ID { get; set; } = Guid.NewGuid();
-
-        public string DifficultyName { get; set; } = string.Empty;
-
-        public RealmRuleset Ruleset { get; set; } = null!;
-
-        public RealmBeatmapDifficulty Difficulty { get; set; } = null!;
-
-        public RealmBeatmapMetadata Metadata { get; set; } = null!;
-
-        public RealmBeatmapSet? BeatmapSet { get; set; }
-
-        [Ignored]
-        public RealmNamedFileUsage? File => BeatmapSet?.Files.First(f => f.File.Hash == Hash);
-
-        public BeatmapOnlineStatus Status
-        {
-            get => (BeatmapOnlineStatus)StatusInt;
-            set => StatusInt = (int)value;
-        }
-
-        [MapTo(nameof(Status))]
-        public int StatusInt { get; set; } = (int)BeatmapOnlineStatus.None;
-
-        [Indexed]
-        public int OnlineID { get; set; } = -1;
-
-        public double Length { get; set; }
-
-        public double BPM { get; set; }
-
-        public string Hash { get; set; } = string.Empty;
-
-        public double StarRating { get; set; }
-
-        public string MD5Hash { get; set; } = string.Empty;
-
-        [JsonIgnore]
-        public bool Hidden { get; set; }
-
-        public RealmBeatmap(RealmRuleset ruleset, RealmBeatmapDifficulty difficulty, RealmBeatmapMetadata metadata)
-        {
-            Ruleset = ruleset;
-            Difficulty = difficulty;
-            Metadata = metadata;
-        }
-
-        [UsedImplicitly]
-        private RealmBeatmap()
-        {
-        }
-
-        #region Properties we may not want persisted (but also maybe no harm?)
-
-        public double AudioLeadIn { get; set; }
-
-        public float StackLeniency { get; set; } = 0.7f;
-
-        public bool SpecialStyle { get; set; }
-
-        public bool LetterboxInBreaks { get; set; }
-
-        public bool WidescreenStoryboard { get; set; }
-
-        public bool EpilepsyWarning { get; set; }
-
-        public bool SamplesMatchPlaybackRate { get; set; }
-
-        public double DistanceSpacing { get; set; }
-
-        public int BeatDivisor { get; set; }
-
-        public int GridSize { get; set; }
-
-        public double TimelineZoom { get; set; }
-
-        #endregion
-
-        public bool Equals(RealmBeatmap? other)
-        {
-            if (ReferenceEquals(this, other)) return true;
-            if (other == null) return false;
-
-            return ID == other.ID;
-        }
-
-        public bool Equals(IBeatmapInfo? other) => other is RealmBeatmap b && Equals(b);
-
-        public bool AudioEquals(RealmBeatmap? other) => other != null
-                                                        && BeatmapSet != null
-                                                        && other.BeatmapSet != null
-                                                        && BeatmapSet.Hash == other.BeatmapSet.Hash
-                                                        && Metadata.AudioFile == other.Metadata.AudioFile;
-
-        public bool BackgroundEquals(RealmBeatmap? other) => other != null
-                                                             && BeatmapSet != null
-                                                             && other.BeatmapSet != null
-                                                             && BeatmapSet.Hash == other.BeatmapSet.Hash
-                                                             && Metadata.BackgroundFile == other.Metadata.BackgroundFile;
-
-        IBeatmapMetadataInfo IBeatmapInfo.Metadata => Metadata;
-        IBeatmapSetInfo? IBeatmapInfo.BeatmapSet => BeatmapSet;
-        IRulesetInfo IBeatmapInfo.Ruleset => Ruleset;
-        IBeatmapDifficultyInfo IBeatmapInfo.Difficulty => Difficulty;
-    }
-}
diff --git a/osu.Game/Models/RealmBeatmapDifficulty.cs b/osu.Game/Models/RealmBeatmapDifficulty.cs
deleted file mode 100644
index 3c1dad69e4..0000000000
--- a/osu.Game/Models/RealmBeatmapDifficulty.cs
+++ /dev/null
@@ -1,45 +0,0 @@
-// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using osu.Framework.Testing;
-using osu.Game.Beatmaps;
-using Realms;
-
-#nullable enable
-
-namespace osu.Game.Models
-{
-    [ExcludeFromDynamicCompile]
-    [MapTo("BeatmapDifficulty")]
-    public class RealmBeatmapDifficulty : EmbeddedObject, IBeatmapDifficultyInfo
-    {
-        public float DrainRate { get; set; } = IBeatmapDifficultyInfo.DEFAULT_DIFFICULTY;
-        public float CircleSize { get; set; } = IBeatmapDifficultyInfo.DEFAULT_DIFFICULTY;
-        public float OverallDifficulty { get; set; } = IBeatmapDifficultyInfo.DEFAULT_DIFFICULTY;
-        public float ApproachRate { get; set; } = IBeatmapDifficultyInfo.DEFAULT_DIFFICULTY;
-
-        public double SliderMultiplier { get; set; } = 1;
-        public double SliderTickRate { get; set; } = 1;
-
-        /// <summary>
-        /// Returns a shallow-clone of this <see cref="RealmBeatmapDifficulty"/>.
-        /// </summary>
-        public RealmBeatmapDifficulty Clone()
-        {
-            var diff = new RealmBeatmapDifficulty();
-            CopyTo(diff);
-            return diff;
-        }
-
-        public void CopyTo(RealmBeatmapDifficulty difficulty)
-        {
-            difficulty.ApproachRate = ApproachRate;
-            difficulty.DrainRate = DrainRate;
-            difficulty.CircleSize = CircleSize;
-            difficulty.OverallDifficulty = OverallDifficulty;
-
-            difficulty.SliderMultiplier = SliderMultiplier;
-            difficulty.SliderTickRate = SliderTickRate;
-        }
-    }
-}
diff --git a/osu.Game/Models/RealmBeatmapMetadata.cs b/osu.Game/Models/RealmBeatmapMetadata.cs
deleted file mode 100644
index db1b09e6ad..0000000000
--- a/osu.Game/Models/RealmBeatmapMetadata.cs
+++ /dev/null
@@ -1,48 +0,0 @@
-// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using System;
-using Newtonsoft.Json;
-using osu.Framework.Testing;
-using osu.Game.Beatmaps;
-using osu.Game.Users;
-using Realms;
-
-#nullable enable
-
-namespace osu.Game.Models
-{
-    [ExcludeFromDynamicCompile]
-    [Serializable]
-    [MapTo("BeatmapMetadata")]
-    public class RealmBeatmapMetadata : RealmObject, IBeatmapMetadataInfo
-    {
-        public string Title { get; set; } = string.Empty;
-
-        [JsonProperty("title_unicode")]
-        public string TitleUnicode { get; set; } = string.Empty;
-
-        public string Artist { get; set; } = string.Empty;
-
-        [JsonProperty("artist_unicode")]
-        public string ArtistUnicode { get; set; } = string.Empty;
-
-        public RealmUser Author { get; set; } = new RealmUser();
-
-        public string Source { get; set; } = string.Empty;
-
-        [JsonProperty(@"tags")]
-        public string Tags { get; set; } = string.Empty;
-
-        /// <summary>
-        /// The time in milliseconds to begin playing the track for preview purposes.
-        /// If -1, the track should begin playing at 40% of its length.
-        /// </summary>
-        public int PreviewTime { get; set; }
-
-        public string AudioFile { get; set; } = string.Empty;
-        public string BackgroundFile { get; set; } = string.Empty;
-
-        IUser IBeatmapMetadataInfo.Author => Author;
-    }
-}
diff --git a/osu.Game/Models/RealmBeatmapSet.cs b/osu.Game/Models/RealmBeatmapSet.cs
deleted file mode 100644
index 3566ff5321..0000000000
--- a/osu.Game/Models/RealmBeatmapSet.cs
+++ /dev/null
@@ -1,81 +0,0 @@
-// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using osu.Framework.Testing;
-using osu.Game.Beatmaps;
-using osu.Game.Database;
-using osu.Game.Extensions;
-using Realms;
-
-#nullable enable
-
-namespace osu.Game.Models
-{
-    [ExcludeFromDynamicCompile]
-    [MapTo("BeatmapSet")]
-    public class RealmBeatmapSet : RealmObject, IHasGuidPrimaryKey, IHasRealmFiles, ISoftDelete, IEquatable<RealmBeatmapSet>, IBeatmapSetInfo
-    {
-        [PrimaryKey]
-        public Guid ID { get; set; } = Guid.NewGuid();
-
-        [Indexed]
-        public int OnlineID { get; set; } = -1;
-
-        public DateTimeOffset DateAdded { get; set; }
-
-        public IBeatmapMetadataInfo Metadata => Beatmaps.FirstOrDefault()?.Metadata ?? new RealmBeatmapMetadata();
-
-        public IList<RealmBeatmap> Beatmaps { get; } = null!;
-
-        public IList<RealmNamedFileUsage> Files { get; } = null!;
-
-        public BeatmapOnlineStatus Status
-        {
-            get => (BeatmapOnlineStatus)StatusInt;
-            set => StatusInt = (int)value;
-        }
-
-        [MapTo(nameof(Status))]
-        public int StatusInt { get; set; } = (int)BeatmapOnlineStatus.None;
-
-        public bool DeletePending { get; set; }
-
-        public string Hash { get; set; } = string.Empty;
-
-        /// <summary>
-        /// Whether deleting this beatmap set should be prohibited (due to it being a system requirement to be present).
-        /// </summary>
-        public bool Protected { get; set; }
-
-        public double MaxStarDifficulty => Beatmaps.Max(b => b.StarRating);
-
-        public double MaxLength => Beatmaps.Max(b => b.Length);
-
-        public double MaxBPM => Beatmaps.Max(b => b.BPM);
-
-        /// <summary>
-        /// Returns the storage path for the file in this beatmapset with the given filename, if any exists, otherwise null.
-        /// The path returned is relative to the user file storage.
-        /// </summary>
-        /// <param name="filename">The name of the file to get the storage path of.</param>
-        public string? GetPathForFile(string filename) => Files.SingleOrDefault(f => string.Equals(f.Filename, filename, StringComparison.OrdinalIgnoreCase))?.File.GetStoragePath();
-
-        public bool Equals(RealmBeatmapSet? other)
-        {
-            if (ReferenceEquals(this, other)) return true;
-            if (other == null) return false;
-
-            return ID == other.ID;
-        }
-
-        public override string ToString() => Metadata.GetDisplayString();
-
-        public bool Equals(IBeatmapSetInfo? other) => other is RealmBeatmapSet b && Equals(b);
-
-        IEnumerable<IBeatmapInfo> IBeatmapSetInfo.Beatmaps => Beatmaps;
-        IEnumerable<INamedFileUsage> IHasNamedFiles.Files => Files;
-    }
-}
diff --git a/osu.Game/Models/RealmRuleset.cs b/osu.Game/Models/RealmRuleset.cs
deleted file mode 100644
index b959d0b4dc..0000000000
--- a/osu.Game/Models/RealmRuleset.cs
+++ /dev/null
@@ -1,88 +0,0 @@
-// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using System;
-using JetBrains.Annotations;
-using osu.Framework.Testing;
-using osu.Game.Rulesets;
-using Realms;
-
-#nullable enable
-
-namespace osu.Game.Models
-{
-    [ExcludeFromDynamicCompile]
-    [MapTo("Ruleset")]
-    public class RealmRuleset : RealmObject, IEquatable<RealmRuleset>, IRulesetInfo
-    {
-        [PrimaryKey]
-        public string ShortName { get; set; } = string.Empty;
-
-        [Indexed]
-        public int OnlineID { get; set; } = -1;
-
-        public string Name { get; set; } = string.Empty;
-
-        public string InstantiationInfo { get; set; } = string.Empty;
-
-        public RealmRuleset(string shortName, string name, string instantiationInfo, int onlineID)
-        {
-            ShortName = shortName;
-            Name = name;
-            InstantiationInfo = instantiationInfo;
-            OnlineID = onlineID;
-        }
-
-        [UsedImplicitly]
-        private RealmRuleset()
-        {
-        }
-
-        public RealmRuleset(int? onlineID, string name, string shortName, bool available)
-        {
-            OnlineID = onlineID ?? -1;
-            Name = name;
-            ShortName = shortName;
-            Available = available;
-        }
-
-        public bool Available { get; set; }
-
-        public bool Equals(RealmRuleset? other) => other != null && OnlineID == other.OnlineID && Available == other.Available && Name == other.Name && InstantiationInfo == other.InstantiationInfo;
-
-        public bool Equals(IRulesetInfo? other) => other is RealmRuleset b && Equals(b);
-
-        public override string ToString() => Name;
-
-        public RealmRuleset Clone() => new RealmRuleset
-        {
-            OnlineID = OnlineID,
-            Name = Name,
-            ShortName = ShortName,
-            InstantiationInfo = InstantiationInfo,
-            Available = Available
-        };
-
-        public Ruleset CreateInstance()
-        {
-            if (!Available)
-                throw new RulesetLoadException(@"Ruleset not available");
-
-            var type = Type.GetType(InstantiationInfo);
-
-            if (type == null)
-                throw new RulesetLoadException(@"Type lookup failure");
-
-            var ruleset = Activator.CreateInstance(type) as Ruleset;
-
-            if (ruleset == null)
-                throw new RulesetLoadException(@"Instantiation failure");
-
-            // overwrite the pre-populated RulesetInfo with a potentially database attached copy.
-            // TODO: figure if we still want/need this after switching to realm.
-            // ruleset.RulesetInfo = this;
-
-            return ruleset;
-        }
-    }
-}
diff --git a/osu.Game/Models/RealmUser.cs b/osu.Game/Models/RealmUser.cs
index 154ece502f..ff35528827 100644
--- a/osu.Game/Models/RealmUser.cs
+++ b/osu.Game/Models/RealmUser.cs
@@ -1,17 +1,26 @@
 // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
 // See the LICENCE file in the repository root for full licence text.
 
+using System;
 using osu.Game.Users;
 using Realms;
 
 namespace osu.Game.Models
 {
-    public class RealmUser : EmbeddedObject, IUser
+    public class RealmUser : EmbeddedObject, IUser, IEquatable<RealmUser>
     {
         public int OnlineID { get; set; } = 1;
 
         public string Username { get; set; }
 
         public bool IsBot => false;
+
+        public bool Equals(RealmUser other)
+        {
+            if (ReferenceEquals(null, other)) return false;
+            if (ReferenceEquals(this, other)) return true;
+
+            return OnlineID == other.OnlineID && Username == other.Username;
+        }
     }
 }
diff --git a/osu.Game/Online/API/Requests/Responses/APIBeatmapSet.cs b/osu.Game/Online/API/Requests/Responses/APIBeatmapSet.cs
index 57c45faed3..d99c13b977 100644
--- a/osu.Game/Online/API/Requests/Responses/APIBeatmapSet.cs
+++ b/osu.Game/Online/API/Requests/Responses/APIBeatmapSet.cs
@@ -7,6 +7,7 @@ using Newtonsoft.Json;
 using osu.Game.Beatmaps;
 using osu.Game.Database;
 using osu.Game.Extensions;
+using osu.Game.Models;
 
 #nullable enable
 
@@ -123,8 +124,11 @@ namespace osu.Game.Online.API.Requests.Responses
             TitleUnicode = TitleUnicode,
             Artist = Artist,
             ArtistUnicode = ArtistUnicode,
-            AuthorID = AuthorID,
-            Author = Author,
+            Author = new RealmUser
+            {
+                OnlineID = Author.OnlineID,
+                Username = Author.Username
+            },
             Source = Source,
             Tags = Tags,
         };
diff --git a/osu.Game/Online/API/Requests/Responses/APIScore.cs b/osu.Game/Online/API/Requests/Responses/APIScore.cs
index 4f795bee6c..d8f4ba835d 100644
--- a/osu.Game/Online/API/Requests/Responses/APIScore.cs
+++ b/osu.Game/Online/API/Requests/Responses/APIScore.cs
@@ -86,7 +86,7 @@ namespace osu.Game.Online.API.Requests.Responses
         /// <returns></returns>
         public ScoreInfo CreateScoreInfo(RulesetStore rulesets, BeatmapInfo beatmap = null)
         {
-            var ruleset = rulesets.GetRuleset(RulesetID);
+            var ruleset = rulesets.GetRuleset(RulesetID) ?? throw new InvalidOperationException();
 
             var rulesetInstance = ruleset.CreateInstance();
 
@@ -99,13 +99,12 @@ namespace osu.Game.Online.API.Requests.Responses
             {
                 TotalScore = TotalScore,
                 MaxCombo = MaxCombo,
-                BeatmapInfo = beatmap,
+                BeatmapInfo = beatmap ?? new BeatmapInfo(),
                 User = User,
                 Accuracy = Accuracy,
                 OnlineID = OnlineID,
                 Date = Date,
                 PP = PP,
-                RulesetID = RulesetID,
                 Hash = HasReplay ? "online" : string.Empty, // todo: temporary?
                 Rank = Rank,
                 Ruleset = ruleset,
diff --git a/osu.Game/Online/API/Requests/Responses/APIUserScoreAggregate.cs b/osu.Game/Online/API/Requests/Responses/APIUserScoreAggregate.cs
index 9a7f0832a6..a298a8625a 100644
--- a/osu.Game/Online/API/Requests/Responses/APIUserScoreAggregate.cs
+++ b/osu.Game/Online/API/Requests/Responses/APIUserScoreAggregate.cs
@@ -1,7 +1,9 @@
 // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
 // See the LICENCE file in the repository root for full licence text.
 
+using System;
 using Newtonsoft.Json;
+using osu.Game.Rulesets.Mods;
 using osu.Game.Scoring;
 
 namespace osu.Game.Online.API.Requests.Responses
@@ -42,7 +44,8 @@ namespace osu.Game.Online.API.Requests.Responses
                 PP = PP,
                 TotalScore = TotalScore,
                 User = User,
-                Position = Position
+                Position = Position,
+                Mods = Array.Empty<Mod>()
             };
     }
 }
diff --git a/osu.Game/Online/BeatmapDownloadTracker.cs b/osu.Game/Online/BeatmapDownloadTracker.cs
index 509d5c1b71..be5bdea6f1 100644
--- a/osu.Game/Online/BeatmapDownloadTracker.cs
+++ b/osu.Game/Online/BeatmapDownloadTracker.cs
@@ -2,8 +2,10 @@
 // See the LICENCE file in the repository root for full licence text.
 
 using System;
+using System.Linq;
 using osu.Framework.Allocation;
 using osu.Game.Beatmaps;
+using osu.Game.Database;
 using osu.Game.Online.API;
 
 #nullable enable
@@ -12,37 +14,47 @@ namespace osu.Game.Online
 {
     public class BeatmapDownloadTracker : DownloadTracker<IBeatmapSetInfo>
     {
-        [Resolved(CanBeNull = true)]
-        protected BeatmapManager? Manager { get; private set; }
-
         [Resolved(CanBeNull = true)]
         protected BeatmapModelDownloader? Downloader { get; private set; }
 
         private ArchiveDownloadRequest<IBeatmapSetInfo>? attachedRequest;
 
+        private IDisposable? realmSubscription;
+
+        [Resolved]
+        private RealmContextFactory realmContextFactory { get; set; } = null!;
+
         public BeatmapDownloadTracker(IBeatmapSetInfo trackedItem)
             : base(trackedItem)
         {
         }
 
-        [BackgroundDependencyLoader(true)]
-        private void load()
+        protected override void LoadComplete()
         {
-            if (Manager == null || Downloader == null)
+            base.LoadComplete();
+
+            if (Downloader == null)
                 return;
 
+            Downloader.DownloadBegan += downloadBegan;
+            Downloader.DownloadFailed += downloadFailed;
+
             // Used to interact with manager classes that don't support interface types. Will eventually be replaced.
             var beatmapSetInfo = new BeatmapSetInfo { OnlineID = TrackedItem.OnlineID };
 
-            if (Manager.IsAvailableLocally(beatmapSetInfo))
-                UpdateState(DownloadState.LocallyAvailable);
-            else
-                attachDownload(Downloader.GetExistingDownload(beatmapSetInfo));
-
-            Downloader.DownloadBegan += downloadBegan;
-            Downloader.DownloadFailed += downloadFailed;
-            Manager.ItemUpdated += itemUpdated;
-            Manager.ItemRemoved += itemRemoved;
+            realmSubscription = realmContextFactory.Context.All<BeatmapSetInfo>().Where(s => s.OnlineID == TrackedItem.OnlineID && !s.DeletePending).QueryAsyncWithNotifications((items, changes, ___) =>
+            {
+                if (items.Any())
+                    Schedule(() => UpdateState(DownloadState.LocallyAvailable));
+                else
+                {
+                    Schedule(() =>
+                    {
+                        UpdateState(DownloadState.NotDownloaded);
+                        attachDownload(Downloader.GetExistingDownload(beatmapSetInfo));
+                    });
+                }
+            });
         }
 
         private void downloadBegan(ArchiveDownloadRequest<IBeatmapSetInfo> request) => Schedule(() =>
@@ -97,18 +109,6 @@ namespace osu.Game.Online
 
         private void onRequestFailure(Exception e) => Schedule(() => attachDownload(null));
 
-        private void itemUpdated(BeatmapSetInfo item) => Schedule(() =>
-        {
-            if (checkEquality(item, TrackedItem))
-                UpdateState(DownloadState.LocallyAvailable);
-        });
-
-        private void itemRemoved(BeatmapSetInfo item) => Schedule(() =>
-        {
-            if (checkEquality(item, TrackedItem))
-                UpdateState(DownloadState.NotDownloaded);
-        });
-
         private bool checkEquality(IBeatmapSetInfo x, IBeatmapSetInfo y) => x.OnlineID == y.OnlineID;
 
         #region Disposal
@@ -118,17 +118,13 @@ namespace osu.Game.Online
             base.Dispose(isDisposing);
             attachDownload(null);
 
+            realmSubscription?.Dispose();
+
             if (Downloader != null)
             {
                 Downloader.DownloadBegan -= downloadBegan;
                 Downloader.DownloadFailed -= downloadFailed;
             }
-
-            if (Manager != null)
-            {
-                Manager.ItemUpdated -= itemUpdated;
-                Manager.ItemRemoved -= itemRemoved;
-            }
         }
 
         #endregion
diff --git a/osu.Game/Online/Leaderboards/LeaderboardScore.cs b/osu.Game/Online/Leaderboards/LeaderboardScore.cs
index 14eec8b388..906e09b8c1 100644
--- a/osu.Game/Online/Leaderboards/LeaderboardScore.cs
+++ b/osu.Game/Online/Leaderboards/LeaderboardScore.cs
@@ -46,14 +46,16 @@ namespace osu.Game.Online.Leaderboards
         protected Container RankContainer { get; private set; }
 
         private readonly int? rank;
-        private readonly bool allowHighlight;
+        private readonly bool isOnlineScope;
 
         private Box background;
         private Container content;
         private Drawable avatar;
         private Drawable scoreRank;
         private OsuSpriteText nameLabel;
-        private GlowingSpriteText scoreLabel;
+
+        public GlowingSpriteText ScoreText { get; private set; }
+
         private Container flagBadgeContainer;
         private FillFlowContainer<ModIcon> modsContainer;
 
@@ -68,12 +70,12 @@ namespace osu.Game.Online.Leaderboards
         [Resolved]
         private Storage storage { get; set; }
 
-        public LeaderboardScore(ScoreInfo score, int? rank, bool allowHighlight = true)
+        public LeaderboardScore(ScoreInfo score, int? rank, bool isOnlineScope = true)
         {
             Score = score;
 
             this.rank = rank;
-            this.allowHighlight = allowHighlight;
+            this.isOnlineScope = isOnlineScope;
 
             RelativeSizeAxes = Axes.X;
             Height = HEIGHT;
@@ -111,7 +113,7 @@ namespace osu.Game.Online.Leaderboards
                                 background = new Box
                                 {
                                     RelativeSizeAxes = Axes.Both,
-                                    Colour = user.OnlineID == api.LocalUser.Value.Id && allowHighlight ? colour.Green : Color4.Black,
+                                    Colour = user.OnlineID == api.LocalUser.Value.Id && isOnlineScope ? colour.Green : Color4.Black,
                                     Alpha = background_alpha,
                                 },
                             },
@@ -198,7 +200,7 @@ namespace osu.Game.Online.Leaderboards
                                     Spacing = new Vector2(5f, 0f),
                                     Children = new Drawable[]
                                     {
-                                        scoreLabel = new GlowingSpriteText
+                                        ScoreText = new GlowingSpriteText
                                         {
                                             TextColour = Color4.White,
                                             GlowColour = Color4Extensions.FromHex(@"83ccfa"),
@@ -240,7 +242,7 @@ namespace osu.Game.Online.Leaderboards
 
         public override void Show()
         {
-            foreach (var d in new[] { avatar, nameLabel, scoreLabel, scoreRank, flagBadgeContainer, modsContainer }.Concat(statisticsLabels))
+            foreach (var d in new[] { avatar, nameLabel, ScoreText, scoreRank, flagBadgeContainer, modsContainer }.Concat(statisticsLabels))
                 d.FadeOut();
 
             Alpha = 0;
@@ -262,7 +264,7 @@ namespace osu.Game.Online.Leaderboards
 
                 using (BeginDelayedSequence(250))
                 {
-                    scoreLabel.FadeIn(200);
+                    ScoreText.FadeIn(200);
                     scoreRank.FadeIn(200);
 
                     using (BeginDelayedSequence(50))
@@ -399,7 +401,7 @@ namespace osu.Game.Online.Leaderboards
                 if (Score.Files.Count > 0)
                     items.Add(new OsuMenuItem("Export", MenuItemType.Standard, () => new LegacyScoreExporter(storage).Export(Score)));
 
-                if (Score.ID != 0)
+                if (!isOnlineScope)
                     items.Add(new OsuMenuItem("Delete", MenuItemType.Destructive, () => dialogOverlay?.Push(new LocalScoreDeleteDialog(Score))));
 
                 return items.ToArray();
diff --git a/osu.Game/Online/Rooms/MultiplayerScore.cs b/osu.Game/Online/Rooms/MultiplayerScore.cs
index 05c9a1b6cf..f1bb57bd9d 100644
--- a/osu.Game/Online/Rooms/MultiplayerScore.cs
+++ b/osu.Game/Online/Rooms/MultiplayerScore.cs
@@ -73,9 +73,7 @@ namespace osu.Game.Online.Rooms
                 TotalScore = TotalScore,
                 MaxCombo = MaxCombo,
                 BeatmapInfo = beatmap,
-                BeatmapInfoID = playlistItem.BeatmapID,
-                Ruleset = rulesets.GetRuleset(playlistItem.RulesetID),
-                RulesetID = playlistItem.RulesetID,
+                Ruleset = rulesets.GetRuleset(playlistItem.RulesetID) ?? throw new InvalidOperationException(),
                 Statistics = Statistics,
                 User = User,
                 Accuracy = Accuracy,
diff --git a/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs b/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs
index a32f069470..1f77b1d383 100644
--- a/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs
+++ b/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs
@@ -3,6 +3,7 @@
 
 using System;
 using System.Diagnostics;
+using System.Linq;
 using osu.Framework.Allocation;
 using osu.Framework.Bindables;
 using osu.Framework.Graphics;
@@ -10,6 +11,8 @@ using osu.Framework.Graphics.Containers;
 using osu.Framework.Logging;
 using osu.Framework.Threading;
 using osu.Game.Beatmaps;
+using osu.Game.Database;
+using Realms;
 
 namespace osu.Game.Online.Rooms
 {
@@ -27,7 +30,7 @@ namespace osu.Game.Online.Rooms
         protected override bool RequiresChildrenUpdate => true;
 
         [Resolved]
-        private BeatmapManager beatmapManager { get; set; }
+        private RealmContextFactory realmContextFactory { get; set; } = null!;
 
         /// <summary>
         /// The availability state of the currently selected playlist item.
@@ -40,10 +43,7 @@ namespace osu.Game.Online.Rooms
 
         private BeatmapDownloadTracker downloadTracker;
 
-        /// <summary>
-        /// The beatmap matching the required hash (and providing a final <see cref="BeatmapAvailability.LocallyAvailable"/> state).
-        /// </summary>
-        private BeatmapInfo matchingHash;
+        private IDisposable realmSubscription;
 
         protected override void LoadComplete()
         {
@@ -64,7 +64,7 @@ namespace osu.Game.Online.Rooms
 
                 AddInternal(downloadTracker);
 
-                downloadTracker.State.BindValueChanged(_ => updateAvailability(), true);
+                downloadTracker.State.BindValueChanged(_ => Scheduler.AddOnce(updateAvailability), true);
                 downloadTracker.Progress.BindValueChanged(_ =>
                 {
                     if (downloadTracker.State.Value != DownloadState.Downloading)
@@ -75,35 +75,24 @@ namespace osu.Game.Online.Rooms
                     if (progressUpdate?.Completed != false)
                         progressUpdate = Scheduler.AddDelayed(updateAvailability, progressUpdate == null ? 0 : 500);
                 }, true);
+
+                // handles changes to hash that didn't occur from the import process (ie. a user editing the beatmap in the editor, somehow).
+                realmSubscription?.Dispose();
+                realmSubscription = filteredBeatmaps().QueryAsyncWithNotifications((items, changes, ___) =>
+                {
+                    if (changes == null)
+                        return;
+
+                    Scheduler.AddOnce(updateAvailability);
+                });
             }, true);
-
-            // These events are needed for a fringe case where a modified/altered beatmap is imported with matching OnlineIDs.
-            // During the import process this will cause the existing beatmap set to be silently deleted and replaced with the new one.
-            // This is not exposed to us via `BeatmapDownloadTracker` so we have to take it into our own hands (as we care about the hash matching).
-            beatmapManager.ItemUpdated += itemUpdated;
-            beatmapManager.ItemRemoved += itemRemoved;
         }
 
-        private void itemUpdated(BeatmapSetInfo item) => Schedule(() =>
-        {
-            if (matchingHash?.BeatmapSet.ID == item.ID || SelectedItem.Value?.Beatmap.Value.BeatmapSet?.OnlineID == item.OnlineID)
-                updateAvailability();
-        });
-
-        private void itemRemoved(BeatmapSetInfo item) => Schedule(() =>
-        {
-            if (matchingHash?.BeatmapSet.ID == item.ID)
-                updateAvailability();
-        });
-
         private void updateAvailability()
         {
-            if (downloadTracker == null)
+            if (downloadTracker == null || SelectedItem.Value == null)
                 return;
 
-            // will be repopulated below if still valid.
-            matchingHash = null;
-
             switch (downloadTracker.State.Value)
             {
                 case DownloadState.NotDownloaded:
@@ -119,9 +108,7 @@ namespace osu.Game.Online.Rooms
                     break;
 
                 case DownloadState.LocallyAvailable:
-                    matchingHash = findMatchingHash();
-
-                    bool hashMatches = matchingHash != null;
+                    bool hashMatches = filteredBeatmaps().Any();
 
                     availability.Value = hashMatches ? BeatmapAvailability.LocallyAvailable() : BeatmapAvailability.NotDownloaded();
 
@@ -136,23 +123,21 @@ namespace osu.Game.Online.Rooms
             }
         }
 
-        private BeatmapInfo findMatchingHash()
+        private IQueryable<BeatmapInfo> filteredBeatmaps()
         {
             int onlineId = SelectedItem.Value.Beatmap.Value.OnlineID;
             string checksum = SelectedItem.Value.Beatmap.Value.MD5Hash;
 
-            return beatmapManager.QueryBeatmap(b => b.OnlineID == onlineId && b.MD5Hash == checksum && !b.BeatmapSet.DeletePending);
+            return realmContextFactory.Context
+                                      .All<BeatmapInfo>()
+                                      .Filter("OnlineID == $0 && MD5Hash == $1 && BeatmapSet.DeletePending == false", onlineId, checksum);
         }
 
         protected override void Dispose(bool isDisposing)
         {
             base.Dispose(isDisposing);
 
-            if (beatmapManager != null)
-            {
-                beatmapManager.ItemUpdated -= itemUpdated;
-                beatmapManager.ItemRemoved -= itemRemoved;
-            }
+            realmSubscription?.Dispose();
         }
     }
 }
diff --git a/osu.Game/Online/ScoreDownloadTracker.cs b/osu.Game/Online/ScoreDownloadTracker.cs
index 68932cc388..b34586567d 100644
--- a/osu.Game/Online/ScoreDownloadTracker.cs
+++ b/osu.Game/Online/ScoreDownloadTracker.cs
@@ -2,7 +2,9 @@
 // See the LICENCE file in the repository root for full licence text.
 
 using System;
+using System.Linq;
 using osu.Framework.Allocation;
+using osu.Game.Database;
 using osu.Game.Extensions;
 using osu.Game.Online.API;
 using osu.Game.Scoring;
@@ -13,23 +15,26 @@ namespace osu.Game.Online
 {
     public class ScoreDownloadTracker : DownloadTracker<ScoreInfo>
     {
-        [Resolved(CanBeNull = true)]
-        protected ScoreManager? Manager { get; private set; }
-
         [Resolved(CanBeNull = true)]
         protected ScoreModelDownloader? Downloader { get; private set; }
 
         private ArchiveDownloadRequest<IScoreInfo>? attachedRequest;
 
+        private IDisposable? realmSubscription;
+
+        [Resolved]
+        private RealmContextFactory realmContextFactory { get; set; } = null!;
+
         public ScoreDownloadTracker(ScoreInfo trackedItem)
             : base(trackedItem)
         {
         }
 
-        [BackgroundDependencyLoader(true)]
-        private void load()
+        protected override void LoadComplete()
         {
-            if (Manager == null || Downloader == null)
+            base.LoadComplete();
+
+            if (Downloader == null)
                 return;
 
             // Used to interact with manager classes that don't support interface types. Will eventually be replaced.
@@ -39,15 +44,22 @@ namespace osu.Game.Online
                 OnlineID = TrackedItem.OnlineID
             };
 
-            if (Manager.IsAvailableLocally(scoreInfo))
-                UpdateState(DownloadState.LocallyAvailable);
-            else
-                attachDownload(Downloader.GetExistingDownload(scoreInfo));
-
             Downloader.DownloadBegan += downloadBegan;
             Downloader.DownloadFailed += downloadFailed;
-            Manager.ItemUpdated += itemUpdated;
-            Manager.ItemRemoved += itemRemoved;
+
+            realmSubscription = realmContextFactory.Context.All<ScoreInfo>().Where(s => ((s.OnlineID > 0 && s.OnlineID == TrackedItem.OnlineID) || s.Hash == TrackedItem.Hash) && !s.DeletePending).QueryAsyncWithNotifications((items, changes, ___) =>
+            {
+                if (items.Any())
+                    Schedule(() => UpdateState(DownloadState.LocallyAvailable));
+                else
+                {
+                    Schedule(() =>
+                    {
+                        UpdateState(DownloadState.NotDownloaded);
+                        attachDownload(Downloader.GetExistingDownload(scoreInfo));
+                    });
+                }
+            });
         }
 
         private void downloadBegan(ArchiveDownloadRequest<IScoreInfo> request) => Schedule(() =>
@@ -102,18 +114,6 @@ namespace osu.Game.Online
 
         private void onRequestFailure(Exception e) => Schedule(() => attachDownload(null));
 
-        private void itemUpdated(ScoreInfo item) => Schedule(() =>
-        {
-            if (checkEquality(item, TrackedItem))
-                UpdateState(DownloadState.LocallyAvailable);
-        });
-
-        private void itemRemoved(ScoreInfo item) => Schedule(() =>
-        {
-            if (checkEquality(item, TrackedItem))
-                UpdateState(DownloadState.NotDownloaded);
-        });
-
         private bool checkEquality(IScoreInfo x, IScoreInfo y) => x.MatchesOnlineID(y);
 
         #region Disposal
@@ -123,17 +123,13 @@ namespace osu.Game.Online
             base.Dispose(isDisposing);
             attachDownload(null);
 
+            realmSubscription?.Dispose();
+
             if (Downloader != null)
             {
                 Downloader.DownloadBegan -= downloadBegan;
                 Downloader.DownloadFailed -= downloadFailed;
             }
-
-            if (Manager != null)
-            {
-                Manager.ItemUpdated -= itemUpdated;
-                Manager.ItemRemoved -= itemRemoved;
-            }
         }
 
         #endregion
diff --git a/osu.Game/Online/Solo/SubmittableScore.cs b/osu.Game/Online/Solo/SubmittableScore.cs
index 5ca5ad9619..4e4dae5157 100644
--- a/osu.Game/Online/Solo/SubmittableScore.cs
+++ b/osu.Game/Online/Solo/SubmittableScore.cs
@@ -10,7 +10,6 @@ using osu.Game.Online.API;
 using osu.Game.Online.API.Requests.Responses;
 using osu.Game.Rulesets.Scoring;
 using osu.Game.Scoring;
-using APIUser = osu.Game.Online.API.Requests.Responses.APIUser;
 
 namespace osu.Game.Online.Solo
 {
diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs
index 21d84a365b..5b3abc54d3 100644
--- a/osu.Game/OsuGame.cs
+++ b/osu.Game/OsuGame.cs
@@ -154,6 +154,8 @@ namespace osu.Game
 
         private MainMenu menuScreen;
 
+        private VersionManager versionManager;
+
         [CanBeNull]
         private IntroScreen introScreen;
 
@@ -437,7 +439,7 @@ namespace osu.Game
         /// </remarks>
         public void PresentBeatmap(IBeatmapSetInfo beatmap, Predicate<BeatmapInfo> difficultyCriteria = null)
         {
-            BeatmapSetInfo databasedSet = null;
+            ILive<BeatmapSetInfo> databasedSet = null;
 
             if (beatmap.OnlineID > 0)
                 databasedSet = BeatmapManager.QueryBeatmapSet(s => s.OnlineID == beatmap.OnlineID);
@@ -451,14 +453,16 @@ namespace osu.Game
                 return;
             }
 
+            var detachedSet = databasedSet.PerformRead(s => s.Detach());
+
             PerformFromScreen(screen =>
             {
                 // Find beatmaps that match our predicate.
-                var beatmaps = databasedSet.Beatmaps.Where(b => difficultyCriteria?.Invoke(b) ?? true).ToList();
+                var beatmaps = detachedSet.Beatmaps.Where(b => difficultyCriteria?.Invoke(b) ?? true).ToList();
 
                 // Use all beatmaps if predicate matched nothing
                 if (beatmaps.Count == 0)
-                    beatmaps = databasedSet.Beatmaps;
+                    beatmaps = detachedSet.Beatmaps.ToList();
 
                 // Prefer recommended beatmap if recommendations are available, else fallback to a sane selection.
                 var selection = difficultyRecommender.GetRecommendedBeatmap(beatmaps)
@@ -481,7 +485,7 @@ namespace osu.Game
         /// Present a score's replay immediately.
         /// The user should have already requested this interactively.
         /// </summary>
-        public void PresentScore(ScoreInfo score, ScorePresentType presentType = ScorePresentType.Results)
+        public void PresentScore(IScoreInfo score, ScorePresentType presentType = ScorePresentType.Results)
         {
             // The given ScoreInfo may have missing properties if it was retrieved from online data. Re-retrieve it from the database
             // to ensure all the required data for presenting a replay are present.
@@ -490,7 +494,8 @@ namespace osu.Game
             if (score.OnlineID > 0)
                 databasedScoreInfo = ScoreManager.Query(s => s.OnlineID == score.OnlineID);
 
-            databasedScoreInfo ??= ScoreManager.Query(s => s.Hash == score.Hash);
+            if (score is ScoreInfo scoreInfo)
+                databasedScoreInfo ??= ScoreManager.Query(s => s.Hash == scoreInfo.Hash);
 
             if (databasedScoreInfo == null)
             {
@@ -743,6 +748,9 @@ namespace osu.Game
             ScreenStack.ScreenPushed += screenPushed;
             ScreenStack.ScreenExited += screenExited;
 
+            if (!args?.Any(a => a == @"--no-version-overlay") ?? true)
+                loadComponentSingleFile(versionManager = new VersionManager { Depth = int.MinValue }, ScreenContainer.Add);
+
             loadComponentSingleFile(osuLogo, logo =>
             {
                 logoContainer.Add(logo);
@@ -1126,10 +1134,16 @@ namespace osu.Game
             {
                 case IntroScreen intro:
                     introScreen = intro;
+                    versionManager?.Show();
                     break;
 
                 case MainMenu menu:
                     menuScreen = menu;
+                    versionManager?.Show();
+                    break;
+
+                default:
+                    versionManager?.Hide();
                     break;
             }
 
diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs
index 9256514a0a..b24fdf2bfe 100644
--- a/osu.Game/OsuGameBase.cs
+++ b/osu.Game/OsuGameBase.cs
@@ -40,7 +40,6 @@ using osu.Game.Rulesets;
 using osu.Game.Rulesets.Mods;
 using osu.Game.Scoring;
 using osu.Game.Skinning;
-using osu.Game.Stores;
 using osu.Game.Utils;
 using RuntimeInfo = osu.Framework.RuntimeInfo;
 
@@ -144,16 +143,12 @@ namespace osu.Game
         private UserLookupCache userCache;
         private BeatmapLookupCache beatmapCache;
 
-        private FileStore fileStore;
-
         private RulesetConfigCache rulesetConfigCache;
 
         private SpectatorClient spectatorClient;
 
         private MultiplayerClient multiplayerClient;
 
-        private DatabaseContextFactory contextFactory;
-
         private RealmContextFactory realmFactory;
 
         protected override Container<Drawable> Content => content;
@@ -166,8 +161,6 @@ namespace osu.Game
 
         private readonly BindableNumber<double> globalTrackVolumeAdjust = new BindableNumber<double>(global_track_volume_adjust);
 
-        private RealmRulesetStore realmRulesetStore;
-
         public OsuGameBase()
         {
             UseDevelopmentServer = DebugUtils.IsDebugBuild;
@@ -191,16 +184,18 @@ namespace osu.Game
 
             Resources.AddStore(new DllResourceStore(OsuResources.ResourceAssembly));
 
-            dependencies.Cache(contextFactory = new DatabaseContextFactory(Storage));
+            DatabaseContextFactory efContextFactory = Storage.Exists(DatabaseContextFactory.DATABASE_NAME)
+                ? new DatabaseContextFactory(Storage)
+                : null;
 
-            runMigrations();
+            dependencies.Cache(realmFactory = new RealmContextFactory(Storage, "client", efContextFactory));
 
-            dependencies.Cache(RulesetStore = new RulesetStore(contextFactory, Storage));
+            dependencies.Cache(RulesetStore = new RulesetStore(realmFactory, Storage));
             dependencies.CacheAs<IRulesetStore>(RulesetStore);
 
-            dependencies.Cache(realmFactory = new RealmContextFactory(Storage, "client", contextFactory));
-
-            new EFToRealmMigrator(contextFactory, realmFactory, LocalConfig).Run();
+            // A non-null context factory means there's still content to migrate.
+            if (efContextFactory != null)
+                new EFToRealmMigrator(efContextFactory, realmFactory, LocalConfig).Run();
 
             dependencies.CacheAs(Storage);
 
@@ -229,32 +224,13 @@ namespace osu.Game
 
             var defaultBeatmap = new DummyWorkingBeatmap(Audio, Textures);
 
-            dependencies.Cache(fileStore = new FileStore(contextFactory, Storage));
-
             // ordering is important here to ensure foreign keys rules are not broken in ModelStore.Cleanup()
-            dependencies.Cache(ScoreManager = new ScoreManager(RulesetStore, () => BeatmapManager, Storage, contextFactory, Scheduler, Host, () => difficultyCache, LocalConfig));
-            dependencies.Cache(BeatmapManager = new BeatmapManager(Storage, contextFactory, RulesetStore, API, Audio, Resources, Host, defaultBeatmap, performOnlineLookups: true));
+            dependencies.Cache(ScoreManager = new ScoreManager(RulesetStore, () => BeatmapManager, Storage, realmFactory, Scheduler, Host, () => difficultyCache, LocalConfig));
+            dependencies.Cache(BeatmapManager = new BeatmapManager(Storage, realmFactory, RulesetStore, API, Audio, Resources, Host, defaultBeatmap, performOnlineLookups: true));
 
             dependencies.Cache(BeatmapDownloader = new BeatmapModelDownloader(BeatmapManager, API));
             dependencies.Cache(ScoreDownloader = new ScoreModelDownloader(ScoreManager, API));
 
-            // the following realm components are not actively used yet, but initialised and kept up to date for initial testing.
-            realmRulesetStore = new RealmRulesetStore(realmFactory, Storage);
-
-            dependencies.Cache(realmRulesetStore);
-
-            // this should likely be moved to ArchiveModelManager when another case appears where it is necessary
-            // to have inter-dependent model managers. this could be obtained with an IHasForeign<T> interface to
-            // allow lookups to be done on the child (ScoreManager in this case) to perform the cascading delete.
-            List<ScoreInfo> getBeatmapScores(BeatmapSetInfo set)
-            {
-                var beatmapIds = BeatmapManager.QueryBeatmaps(b => b.BeatmapSetInfoID == set.ID).Select(b => b.ID).ToList();
-                return ScoreManager.QueryScores(s => beatmapIds.Contains(s.BeatmapInfo.ID)).ToList();
-            }
-
-            BeatmapManager.ItemRemoved += item => ScoreManager.Delete(getBeatmapScores(item), true);
-            BeatmapManager.ItemUpdated += item => ScoreManager.Undelete(getBeatmapScores(item), true);
-
             dependencies.Cache(difficultyCache = new BeatmapDifficultyCache());
             AddInternal(difficultyCache);
 
@@ -291,8 +267,6 @@ namespace osu.Game
             dependencies.CacheAs<IBindable<WorkingBeatmap>>(Beatmap);
             dependencies.CacheAs(Beatmap);
 
-            fileStore.Cleanup();
-
             // add api components to hierarchy.
             if (API is APIAccess apiAccess)
                 AddInternal(apiAccess);
@@ -327,6 +301,7 @@ namespace osu.Game
             dependencies.CacheAs(MusicController);
 
             Ruleset.BindValueChanged(onRulesetChanged);
+            Beatmap.BindValueChanged(onBeatmapChanged);
         }
 
         protected virtual void InitialiseFonts()
@@ -423,7 +398,6 @@ namespace osu.Game
                 Scheduler.Add(() =>
                 {
                     realmBlocker = realmFactory.BlockAllOperations();
-                    contextFactory.FlushConnections();
 
                     readyToRun.Set();
                 }, false);
@@ -448,9 +422,32 @@ namespace osu.Game
 
         protected override Storage CreateStorage(GameHost host, Storage defaultStorage) => new OsuStorage(host, defaultStorage);
 
+        private void onBeatmapChanged(ValueChangedEvent<WorkingBeatmap> valueChangedEvent)
+        {
+            if (IsLoaded && !ThreadSafety.IsUpdateThread)
+                throw new InvalidOperationException("Global beatmap bindable must be changed from update thread.");
+        }
+
         private void onRulesetChanged(ValueChangedEvent<RulesetInfo> r)
         {
-            if (r.NewValue?.Available != true)
+            if (IsLoaded && !ThreadSafety.IsUpdateThread)
+                throw new InvalidOperationException("Global ruleset bindable must be changed from update thread.");
+
+            Ruleset instance = null;
+
+            try
+            {
+                if (r.NewValue?.Available == true)
+                {
+                    instance = r.NewValue.CreateInstance();
+                }
+            }
+            catch (Exception e)
+            {
+                Logger.Error(e, "Ruleset load failed and has been rolled back");
+            }
+
+            if (instance == null)
             {
                 // reject the change if the ruleset is not available.
                 Ruleset.Value = r.OldValue?.Available == true ? r.OldValue : RulesetStore.AvailableRulesets.First();
@@ -460,7 +457,9 @@ namespace osu.Game
             var dict = new Dictionary<ModType, IReadOnlyList<Mod>>();
 
             foreach (ModType type in Enum.GetValues(typeof(ModType)))
-                dict[type] = r.NewValue.CreateInstance().GetModsFor(type).ToList();
+            {
+                dict[type] = instance.GetModsFor(type).ToList();
+            }
 
             if (!SelectedMods.Disabled)
                 SelectedMods.Value = Array.Empty<Mod>();
@@ -468,29 +467,6 @@ namespace osu.Game
             AvailableMods.Value = dict;
         }
 
-        private void runMigrations()
-        {
-            try
-            {
-                using (var db = contextFactory.GetForWrite(false))
-                    db.Context.Migrate();
-            }
-            catch (Exception e)
-            {
-                Logger.Error(e.InnerException ?? e, "Migration failed! We'll be starting with a fresh database.", LoggingTarget.Database);
-
-                // if we failed, let's delete the database and start fresh.
-                // todo: we probably want a better (non-destructive) migrations/recovery process at a later point than this.
-                contextFactory.ResetDatabase();
-
-                Logger.Log("Database purged successfully.", LoggingTarget.Database);
-
-                // only run once more, then hard bail.
-                using (var db = contextFactory.GetForWrite(false))
-                    db.Context.Migrate();
-            }
-        }
-
         protected override void Dispose(bool isDisposing)
         {
             base.Dispose(isDisposing);
@@ -499,9 +475,6 @@ namespace osu.Game
             BeatmapManager?.Dispose();
             LocalConfig?.Dispose();
 
-            contextFactory?.FlushConnections();
-
-            realmRulesetStore?.Dispose();
             realmFactory?.Dispose();
         }
     }
diff --git a/osu.Game/Overlays/AccountCreation/ScreenEntry.cs b/osu.Game/Overlays/AccountCreation/ScreenEntry.cs
index a11b234cb1..a2c04c6989 100644
--- a/osu.Game/Overlays/AccountCreation/ScreenEntry.cs
+++ b/osu.Game/Overlays/AccountCreation/ScreenEntry.cs
@@ -44,7 +44,7 @@ namespace osu.Game.Overlays.AccountCreation
         private GameHost host { get; set; }
 
         [BackgroundDependencyLoader]
-        private void load(OsuColour colours)
+        private void load()
         {
             InternalChildren = new Drawable[]
             {
diff --git a/osu.Game/Overlays/AccountCreationOverlay.cs b/osu.Game/Overlays/AccountCreationOverlay.cs
index 3084c7475a..a96aff2a5d 100644
--- a/osu.Game/Overlays/AccountCreationOverlay.cs
+++ b/osu.Game/Overlays/AccountCreationOverlay.cs
@@ -10,7 +10,6 @@ using osu.Framework.Graphics.Effects;
 using osu.Framework.Graphics.Shapes;
 using osu.Framework.Screens;
 using osu.Framework.Threading;
-using osu.Game.Graphics;
 using osu.Game.Graphics.Containers;
 using osu.Game.Online.API;
 using osu.Game.Overlays.AccountCreation;
@@ -35,7 +34,7 @@ namespace osu.Game.Overlays
         private readonly IBindable<APIState> apiState = new Bindable<APIState>();
 
         [BackgroundDependencyLoader]
-        private void load(OsuColour colours, IAPIProvider api)
+        private void load(IAPIProvider api)
         {
             apiState.BindTo(api.State);
             apiState.BindValueChanged(apiStateChanged, true);
diff --git a/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs b/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs
index 2c78fa264e..5ef434c427 100644
--- a/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs
+++ b/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs
@@ -173,7 +173,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
                 {
                     Text = score.MaxCombo.ToLocalisableString(@"0\x"),
                     Font = OsuFont.GetFont(size: text_size),
-                    Colour = score.MaxCombo == score.BeatmapInfo?.MaxCombo ? highAccuracyColour : Color4.White
+                    Colour = score.MaxCombo == score.BeatmapInfo.MaxCombo ? highAccuracyColour : Color4.White
                 }
             };
 
diff --git a/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs b/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs
index a40f29abf2..00dedc892b 100644
--- a/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs
+++ b/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs
@@ -65,6 +65,9 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
                 scoreTable.ClearScores();
                 scoreTable.Hide();
 
+                loading.Hide();
+                loading.FinishTransforms();
+
                 if (value?.Scores.Any() != true)
                     return;
 
@@ -258,9 +261,6 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
             {
                 Scores = null;
                 notSupporterPlaceholder.Show();
-
-                loading.Hide();
-                loading.FinishTransforms();
                 return;
             }
 
@@ -272,9 +272,6 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
             getScoresRequest = new GetScoresRequest(Beatmap.Value, Beatmap.Value.Ruleset, scope.Value, modSelector.SelectedMods);
             getScoresRequest.Success += scores =>
             {
-                loading.Hide();
-                loading.FinishTransforms();
-
                 Scores = scores;
 
                 if (!scores.Scores.Any())
diff --git a/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs b/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs
index 630aa8fe53..1f3f73a60a 100644
--- a/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs
+++ b/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs
@@ -107,7 +107,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
         {
             set
             {
-                if (score == value)
+                if (score?.Equals(value) == true)
                     return;
 
                 score = value;
@@ -115,7 +115,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
                 accuracyColumn.Text = value.DisplayAccuracy;
                 maxComboColumn.Text = value.MaxCombo.ToLocalisableString(@"0\x");
 
-                ppColumn.Alpha = value.BeatmapInfo?.Status.GrantsPerformancePoints() == true ? 1 : 0;
+                ppColumn.Alpha = value.BeatmapInfo.Status.GrantsPerformancePoints() ? 1 : 0;
                 ppColumn.Text = value.PP?.ToLocalisableString(@"N0");
 
                 statisticsColumns.ChildrenEnumerable = value.GetStatisticsForDisplay().Select(createStatisticsColumn);
diff --git a/osu.Game/Overlays/Changelog/ChangelogBuild.cs b/osu.Game/Overlays/Changelog/ChangelogBuild.cs
index 2d071b7345..c65eefdee4 100644
--- a/osu.Game/Overlays/Changelog/ChangelogBuild.cs
+++ b/osu.Game/Overlays/Changelog/ChangelogBuild.cs
@@ -46,7 +46,7 @@ namespace osu.Game.Overlays.Changelog
         }
 
         [BackgroundDependencyLoader]
-        private void load(OsuColour colours, OverlayColourProvider colourProvider)
+        private void load()
         {
             foreach (var categoryEntries in Build.ChangelogEntries.GroupBy(b => b.Category).OrderBy(c => c.Key))
             {
diff --git a/osu.Game/Overlays/ChangelogOverlay.cs b/osu.Game/Overlays/ChangelogOverlay.cs
index fe611d0134..ab97ae950d 100644
--- a/osu.Game/Overlays/ChangelogOverlay.cs
+++ b/osu.Game/Overlays/ChangelogOverlay.cs
@@ -7,7 +7,6 @@ using System.Threading;
 using System.Threading.Tasks;
 using JetBrains.Annotations;
 using osu.Framework.Allocation;
-using osu.Framework.Audio;
 using osu.Framework.Bindables;
 using osu.Framework.Graphics;
 using osu.Framework.Input.Events;
@@ -35,7 +34,7 @@ namespace osu.Game.Overlays
         }
 
         [BackgroundDependencyLoader]
-        private void load(AudioManager audio)
+        private void load()
         {
             Header.Build.BindTarget = Current;
 
diff --git a/osu.Game/Overlays/Comments/CommentsContainer.cs b/osu.Game/Overlays/Comments/CommentsContainer.cs
index 970fc5ccef..6a5734b553 100644
--- a/osu.Game/Overlays/Comments/CommentsContainer.cs
+++ b/osu.Game/Overlays/Comments/CommentsContainer.cs
@@ -317,7 +317,7 @@ namespace osu.Game.Overlays.Comments
         private class NoCommentsPlaceholder : CompositeDrawable
         {
             [BackgroundDependencyLoader]
-            private void load(OverlayColourProvider colourProvider)
+            private void load()
             {
                 Height = 80;
                 RelativeSizeAxes = Axes.X;
diff --git a/osu.Game/Overlays/MusicController.cs b/osu.Game/Overlays/MusicController.cs
index 3346c6d97d..70f8332295 100644
--- a/osu.Game/Overlays/MusicController.cs
+++ b/osu.Game/Overlays/MusicController.cs
@@ -12,10 +12,11 @@ using osu.Framework.Bindables;
 using osu.Framework.Graphics;
 using osu.Framework.Graphics.Audio;
 using osu.Framework.Graphics.Containers;
-using osu.Framework.Utils;
 using osu.Framework.Threading;
 using osu.Game.Beatmaps;
+using osu.Game.Database;
 using osu.Game.Rulesets.Mods;
+using Realms;
 
 namespace osu.Game.Overlays
 {
@@ -24,6 +25,8 @@ namespace osu.Game.Overlays
     /// </summary>
     public class MusicController : CompositeDrawable
     {
+        private IDisposable beatmapSubscription;
+
         [Resolved]
         private BeatmapManager beatmaps { get; set; }
 
@@ -65,20 +68,46 @@ namespace osu.Game.Overlays
         [NotNull]
         public DrawableTrack CurrentTrack { get; private set; } = new DrawableTrack(new TrackVirtual(1000));
 
+        [Resolved]
+        private RealmContextFactory realmFactory { get; set; }
+
         [BackgroundDependencyLoader]
         private void load()
         {
-            beatmaps.ItemUpdated += beatmapUpdated;
-            beatmaps.ItemRemoved += beatmapRemoved;
-
-            beatmapSets.AddRange(beatmaps.GetAllUsableBeatmapSets(IncludedDetails.Minimal, true).OrderBy(_ => RNG.Next()));
-
             // Todo: These binds really shouldn't be here, but are unlikely to cause any issues for now.
             // They are placed here for now since some tests rely on setting the beatmap _and_ their hierarchies inside their load(), which runs before the MusicController's load().
             beatmap.BindValueChanged(beatmapChanged, true);
             mods.BindValueChanged(_ => ResetTrackAdjustments(), true);
         }
 
+        protected override void LoadComplete()
+        {
+            base.LoadComplete();
+
+            var availableBeatmaps = realmFactory.Context
+                                                .All<BeatmapSetInfo>()
+                                                .Where(s => !s.DeletePending);
+
+            // ensure we're ready before completing async load.
+            // probably not a good way of handling this (as there is a period we aren't watching for changes until the realm subscription finishes up.
+            foreach (var s in availableBeatmaps)
+                beatmapSets.Add(s);
+
+            beatmapSubscription = availableBeatmaps.QueryAsyncWithNotifications(beatmapsChanged);
+        }
+
+        private void beatmapsChanged(IRealmCollection<BeatmapSetInfo> sender, ChangeSet changes, Exception error)
+        {
+            if (changes == null)
+                return;
+
+            foreach (int i in changes.InsertedIndices)
+                beatmapSets.Insert(i, sender[i].Detach());
+
+            foreach (int i in changes.DeletedIndices.OrderByDescending(i => i))
+                beatmapSets.RemoveAt(i);
+        }
+
         /// <summary>
         /// Forcefully reload the current <see cref="WorkingBeatmap"/>'s track from disk.
         /// </summary>
@@ -105,14 +134,6 @@ namespace osu.Game.Overlays
         /// </summary>
         public bool TrackLoaded => CurrentTrack.TrackLoaded;
 
-        private void beatmapUpdated(BeatmapSetInfo set) => Schedule(() =>
-        {
-            beatmapSets.Remove(set);
-            beatmapSets.Add(set);
-        });
-
-        private void beatmapRemoved(BeatmapSetInfo set) => Schedule(() => beatmapSets.RemoveAll(s => s.Equals(set)));
-
         private ScheduledDelegate seekDelegate;
 
         public void SeekTo(double position)
@@ -259,11 +280,12 @@ namespace osu.Game.Overlays
 
             queuedDirection = TrackChangeDirection.Next;
 
-            var playable = BeatmapSets.SkipWhile(i => !i.Equals(current.BeatmapSetInfo)).ElementAtOrDefault(1) ?? BeatmapSets.FirstOrDefault();
+            var playableSet = BeatmapSets.SkipWhile(i => i.ID != current.BeatmapSetInfo.ID).ElementAtOrDefault(1) ?? BeatmapSets.FirstOrDefault();
+            var playableBeatmap = playableSet?.Beatmaps.FirstOrDefault();
 
-            if (playable != null)
+            if (playableBeatmap != null)
             {
-                changeBeatmap(beatmaps.GetWorkingBeatmap(playable.Beatmaps.First()));
+                changeBeatmap(beatmaps.GetWorkingBeatmap(playableBeatmap));
                 restartTrack();
                 return true;
             }
@@ -429,11 +451,7 @@ namespace osu.Game.Overlays
         {
             base.Dispose(isDisposing);
 
-            if (beatmaps != null)
-            {
-                beatmaps.ItemUpdated -= beatmapUpdated;
-                beatmaps.ItemRemoved -= beatmapRemoved;
-            }
+            beatmapSubscription?.Dispose();
         }
     }
 
diff --git a/osu.Game/Overlays/Profile/Header/CentreHeaderContainer.cs b/osu.Game/Overlays/Profile/Header/CentreHeaderContainer.cs
index a13f5ed6ce..00a866f1f4 100644
--- a/osu.Game/Overlays/Profile/Header/CentreHeaderContainer.cs
+++ b/osu.Game/Overlays/Profile/Header/CentreHeaderContainer.cs
@@ -7,7 +7,6 @@ using osu.Framework.Extensions.LocalisationExtensions;
 using osu.Framework.Graphics;
 using osu.Framework.Graphics.Containers;
 using osu.Framework.Graphics.Shapes;
-using osu.Framework.Graphics.Textures;
 using osu.Framework.Localisation;
 using osu.Game.Online.API.Requests.Responses;
 using osu.Game.Overlays.Profile.Header.Components;
@@ -30,7 +29,7 @@ namespace osu.Game.Overlays.Profile.Header
         }
 
         [BackgroundDependencyLoader]
-        private void load(OverlayColourProvider colourProvider, TextureStore textures)
+        private void load(OverlayColourProvider colourProvider)
         {
             Container<Drawable> hiddenDetailContainer;
             Container<Drawable> expandedDetailContainer;
diff --git a/osu.Game/Overlays/Profile/Sections/Ranks/DrawableProfileScore.cs b/osu.Game/Overlays/Profile/Sections/Ranks/DrawableProfileScore.cs
index 562be0403e..998f5d158e 100644
--- a/osu.Game/Overlays/Profile/Sections/Ranks/DrawableProfileScore.cs
+++ b/osu.Game/Overlays/Profile/Sections/Ranks/DrawableProfileScore.cs
@@ -1,6 +1,7 @@
 // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
 // See the LICENCE file in the repository root for full licence text.
 
+using System;
 using System.Diagnostics;
 using System.Linq;
 using JetBrains.Annotations;
@@ -131,9 +132,14 @@ namespace osu.Game.Overlays.Profile.Sections.Ranks
                                         Origin = Anchor.CentreRight,
                                         Direction = FillDirection.Horizontal,
                                         Spacing = new Vector2(2),
-                                        Children = Score.Mods.Select(mod => new ModIcon(rulesets.GetRuleset(Score.RulesetID).CreateInstance().CreateModFromAcronym(mod.Acronym))
+                                        Children = Score.Mods.Select(mod =>
                                         {
-                                            Scale = new Vector2(0.35f)
+                                            var ruleset = rulesets.GetRuleset(Score.RulesetID) ?? throw new InvalidOperationException();
+
+                                            return new ModIcon(ruleset.CreateInstance().CreateModFromAcronym(mod.Acronym))
+                                            {
+                                                Scale = new Vector2(0.35f)
+                                            };
                                         }).ToList(),
                                     }
                                 }
diff --git a/osu.Game/Overlays/Settings/Sections/DebugSettings/MemorySettings.cs b/osu.Game/Overlays/Settings/Sections/DebugSettings/MemorySettings.cs
index eb6e48dfbf..8d4fc5fc9f 100644
--- a/osu.Game/Overlays/Settings/Sections/DebugSettings/MemorySettings.cs
+++ b/osu.Game/Overlays/Settings/Sections/DebugSettings/MemorySettings.cs
@@ -2,7 +2,6 @@
 // See the LICENCE file in the repository root for full licence text.
 
 using osu.Framework.Allocation;
-using osu.Framework.Configuration;
 using osu.Framework.Graphics;
 using osu.Framework.Localisation;
 using osu.Framework.Platform;
@@ -16,7 +15,7 @@ namespace osu.Game.Overlays.Settings.Sections.DebugSettings
         protected override LocalisableString Header => DebugSettingsStrings.MemoryHeader;
 
         [BackgroundDependencyLoader]
-        private void load(FrameworkDebugConfigManager config, GameHost host, RealmContextFactory realmFactory)
+        private void load(GameHost host, RealmContextFactory realmFactory)
         {
             Children = new Drawable[]
             {
diff --git a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs
index 115a7bdc79..94c7c66538 100644
--- a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs
+++ b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs
@@ -18,7 +18,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input
     {
         protected IEnumerable<Framework.Input.Bindings.KeyBinding> Defaults;
 
-        protected RulesetInfo Ruleset;
+        public RulesetInfo Ruleset { get; protected set; }
 
         private readonly int? variant;
 
diff --git a/osu.Game/Overlays/Settings/Sections/Input/RulesetBindingsSection.cs b/osu.Game/Overlays/Settings/Sections/Input/RulesetBindingsSection.cs
index b5d26d4887..dae276c711 100644
--- a/osu.Game/Overlays/Settings/Sections/Input/RulesetBindingsSection.cs
+++ b/osu.Game/Overlays/Settings/Sections/Input/RulesetBindingsSection.cs
@@ -1,6 +1,7 @@
 // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
 // See the LICENCE file in the repository root for full licence text.
 
+using System.Diagnostics;
 using osu.Framework.Graphics;
 using osu.Framework.Graphics.Sprites;
 using osu.Framework.Localisation;
@@ -26,6 +27,8 @@ namespace osu.Game.Overlays.Settings.Sections.Input
 
             var r = ruleset.CreateInstance();
 
+            Debug.Assert(r != null);
+
             foreach (int variant in r.AvailableVariants)
                 Add(new VariantBindingsSubsection(ruleset, variant));
         }
diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs
index 98ccbf85fd..aa02d086f4 100644
--- a/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs
+++ b/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs
@@ -1,7 +1,6 @@
 // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
 // See the LICENCE file in the repository root for full licence text.
 
-using System.Linq;
 using System.Threading.Tasks;
 using JetBrains.Annotations;
 using osu.Framework.Allocation;
@@ -54,7 +53,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
                     dialogOverlay?.Push(new MassDeleteConfirmationDialog(() =>
                     {
                         deleteBeatmapsButton.Enabled.Value = false;
-                        Task.Run(() => beatmaps.Delete(beatmaps.GetAllUsableBeatmapSets())).ContinueWith(t => Schedule(() => deleteBeatmapsButton.Enabled.Value = true));
+                        Task.Run(() => beatmaps.Delete()).ContinueWith(t => Schedule(() => deleteBeatmapsButton.Enabled.Value = true));
                     }));
                 }
             });
@@ -80,7 +79,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
                     dialogOverlay?.Push(new MassDeleteConfirmationDialog(() =>
                     {
                         deleteScoresButton.Enabled.Value = false;
-                        Task.Run(() => scores.Delete(scores.GetAllUsableScores())).ContinueWith(t => Schedule(() => deleteScoresButton.Enabled.Value = true));
+                        Task.Run(() => scores.Delete()).ContinueWith(t => Schedule(() => deleteScoresButton.Enabled.Value = true));
                     }));
                 }
             });
@@ -106,10 +105,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
                     dialogOverlay?.Push(new MassDeleteConfirmationDialog(() =>
                     {
                         deleteSkinsButton.Enabled.Value = false;
-                        Task.Run(() =>
-                        {
-                            skins.Delete();
-                        }).ContinueWith(t => Schedule(() => deleteSkinsButton.Enabled.Value = true));
+                        Task.Run(() => skins.Delete()).ContinueWith(t => Schedule(() => deleteSkinsButton.Enabled.Value = true));
                     }));
                 }
             });
@@ -147,11 +143,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
                     Action = () =>
                     {
                         restoreButton.Enabled.Value = false;
-                        Task.Run(() =>
-                        {
-                            foreach (var b in beatmaps.QueryBeatmaps(b => b.Hidden).ToList())
-                                beatmaps.Restore(b);
-                        }).ContinueWith(t => Schedule(() => restoreButton.Enabled.Value = true));
+                        Task.Run(beatmaps.RestoreAll).ContinueWith(t => Schedule(() => restoreButton.Enabled.Value = true));
                     }
                 },
                 undeleteButton = new SettingsButton
@@ -160,7 +152,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
                     Action = () =>
                     {
                         undeleteButton.Enabled.Value = false;
-                        Task.Run(() => beatmaps.Undelete(beatmaps.QueryBeatmapSets(b => b.DeletePending).ToList())).ContinueWith(t => Schedule(() => undeleteButton.Enabled.Value = true));
+                        Task.Run(beatmaps.UndeleteAll).ContinueWith(t => Schedule(() => undeleteButton.Enabled.Value = true));
                     }
                 },
             });
diff --git a/osu.Game/Overlays/Settings/SettingsFooter.cs b/osu.Game/Overlays/Settings/SettingsFooter.cs
index ed49ce2b63..263f2f4829 100644
--- a/osu.Game/Overlays/Settings/SettingsFooter.cs
+++ b/osu.Game/Overlays/Settings/SettingsFooter.cs
@@ -19,7 +19,7 @@ namespace osu.Game.Overlays.Settings
     public class SettingsFooter : FillFlowContainer
     {
         [BackgroundDependencyLoader]
-        private void load(OsuGameBase game, OsuColour colours, RulesetStore rulesets)
+        private void load(OsuGameBase game, RulesetStore rulesets)
         {
             RelativeSizeAxes = Axes.X;
             AutoSizeAxes = Axes.Y;
diff --git a/osu.Game/Overlays/Settings/SettingsNumberBox.cs b/osu.Game/Overlays/Settings/SettingsNumberBox.cs
index cc4446033a..d931c53e73 100644
--- a/osu.Game/Overlays/Settings/SettingsNumberBox.cs
+++ b/osu.Game/Overlays/Settings/SettingsNumberBox.cs
@@ -68,6 +68,8 @@ namespace osu.Game.Overlays.Settings
 
         private class OutlinedNumberBox : OutlinedTextBox
         {
+            protected override bool AllowIme => false;
+
             protected override bool CanAddCharacter(char character) => character.IsAsciiDigit();
 
             public new void NotifyInputError() => base.NotifyInputError();
diff --git a/osu.Game/Overlays/Settings/SettingsSection.cs b/osu.Game/Overlays/Settings/SettingsSection.cs
index 0ae353602e..2539c32806 100644
--- a/osu.Game/Overlays/Settings/SettingsSection.cs
+++ b/osu.Game/Overlays/Settings/SettingsSection.cs
@@ -65,7 +65,7 @@ namespace osu.Game.Overlays.Settings
         }
 
         [BackgroundDependencyLoader]
-        private void load(OverlayColourProvider colourProvider, OsuColour colours)
+        private void load(OverlayColourProvider colourProvider)
         {
             AddRangeInternal(new Drawable[]
             {
diff --git a/osu.Game/Overlays/Toolbar/Toolbar.cs b/osu.Game/Overlays/Toolbar/Toolbar.cs
index dc0b06b255..776f7ad7b7 100644
--- a/osu.Game/Overlays/Toolbar/Toolbar.cs
+++ b/osu.Game/Overlays/Toolbar/Toolbar.cs
@@ -58,8 +58,11 @@ namespace osu.Game.Overlays.Toolbar
             AlwaysPresent = false;
         }
 
+        [Resolved]
+        private Bindable<RulesetInfo> ruleset { get; set; }
+
         [BackgroundDependencyLoader(true)]
-        private void load(OsuGame osuGame, Bindable<RulesetInfo> parentRuleset)
+        private void load(OsuGame osuGame)
         {
             Children = new Drawable[]
             {
@@ -106,13 +109,17 @@ namespace osu.Game.Overlays.Toolbar
                 }
             };
 
-            // Bound after the selector is added to the hierarchy to give it a chance to load the available rulesets
-            rulesetSelector.Current.BindTo(parentRuleset);
-
             if (osuGame != null)
                 OverlayActivationMode.BindTo(osuGame.OverlayActivationMode);
         }
 
+        protected override void LoadComplete()
+        {
+            base.LoadComplete();
+
+            rulesetSelector.Current.BindTo(ruleset);
+        }
+
         public class ToolbarBackground : Container
         {
             private readonly Box gradientBackground;
diff --git a/osu.Desktop/Overlays/VersionManager.cs b/osu.Game/Overlays/VersionManager.cs
similarity index 98%
rename from osu.Desktop/Overlays/VersionManager.cs
rename to osu.Game/Overlays/VersionManager.cs
index e4a3451651..fe6613fba2 100644
--- a/osu.Desktop/Overlays/VersionManager.cs
+++ b/osu.Game/Overlays/VersionManager.cs
@@ -7,13 +7,12 @@ using osu.Framework.Graphics;
 using osu.Framework.Graphics.Containers;
 using osu.Framework.Graphics.Sprites;
 using osu.Framework.Graphics.Textures;
-using osu.Game;
 using osu.Game.Graphics;
 using osu.Game.Graphics.Sprites;
 using osuTK;
 using osuTK.Graphics;
 
-namespace osu.Desktop.Overlays
+namespace osu.Game.Overlays
 {
     public class VersionManager : VisibilityContainer
     {
diff --git a/osu.Game/Rulesets/EFRulesetInfo.cs b/osu.Game/Rulesets/EFRulesetInfo.cs
new file mode 100644
index 0000000000..473b7c657e
--- /dev/null
+++ b/osu.Game/Rulesets/EFRulesetInfo.cs
@@ -0,0 +1,81 @@
+// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.ComponentModel.DataAnnotations.Schema;
+using System.Diagnostics.CodeAnalysis;
+using Newtonsoft.Json;
+using osu.Framework.Testing;
+
+namespace osu.Game.Rulesets
+{
+    [ExcludeFromDynamicCompile]
+    [Table(@"RulesetInfo")]
+    public sealed class EFRulesetInfo : IEquatable<EFRulesetInfo>, IRulesetInfo
+    {
+        public int? ID { get; set; }
+
+        public string Name { get; set; }
+
+        public string ShortName { get; set; }
+
+        public string InstantiationInfo { get; set; }
+
+        [JsonIgnore]
+        public bool Available { get; set; }
+
+        // TODO: this should probably be moved to RulesetStore.
+        public Ruleset CreateInstance()
+        {
+            if (!Available)
+                throw new RulesetLoadException(@"Ruleset not available");
+
+            var type = Type.GetType(InstantiationInfo);
+
+            if (type == null)
+                throw new RulesetLoadException(@"Type lookup failure");
+
+            var ruleset = Activator.CreateInstance(type) as Ruleset;
+
+            if (ruleset == null)
+                throw new RulesetLoadException(@"Instantiation failure");
+
+            // overwrite the pre-populated RulesetInfo with a potentially database attached copy.
+            // ruleset.RulesetInfo = this;
+
+            return ruleset;
+        }
+
+        public bool Equals(EFRulesetInfo other) => other != null && ID == other.ID && Available == other.Available && Name == other.Name && InstantiationInfo == other.InstantiationInfo;
+
+        public override bool Equals(object obj) => obj is EFRulesetInfo rulesetInfo && Equals(rulesetInfo);
+
+        public bool Equals(IRulesetInfo other) => other is RulesetInfo b && Equals(b);
+
+        [SuppressMessage("ReSharper", "NonReadonlyMemberInGetHashCode")]
+        public override int GetHashCode()
+        {
+            unchecked
+            {
+                int hashCode = ID.HasValue ? ID.GetHashCode() : 0;
+                hashCode = (hashCode * 397) ^ (InstantiationInfo != null ? InstantiationInfo.GetHashCode() : 0);
+                hashCode = (hashCode * 397) ^ (Name != null ? Name.GetHashCode() : 0);
+                hashCode = (hashCode * 397) ^ Available.GetHashCode();
+                return hashCode;
+            }
+        }
+
+        public override string ToString() => Name ?? $"{Name} ({ShortName}) ID: {ID}";
+
+        #region Implementation of IHasOnlineID
+
+        [NotMapped]
+        public int OnlineID
+        {
+            get => ID ?? -1;
+            set => ID = value >= 0 ? value : (int?)null;
+        }
+
+        #endregion
+    }
+}
diff --git a/osu.Game/Rulesets/Edit/Checks/CheckAudioInVideo.cs b/osu.Game/Rulesets/Edit/Checks/CheckAudioInVideo.cs
index 255671c807..a285979fd2 100644
--- a/osu.Game/Rulesets/Edit/Checks/CheckAudioInVideo.cs
+++ b/osu.Game/Rulesets/Edit/Checks/CheckAudioInVideo.cs
@@ -42,7 +42,7 @@ namespace osu.Game.Rulesets.Edit.Checks
 
             foreach (string filename in videoPaths)
             {
-                string storagePath = beatmapSet.GetPathForFile(filename);
+                string storagePath = beatmapSet?.GetPathForFile(filename);
 
                 if (storagePath == null)
                 {
diff --git a/osu.Game/Rulesets/Edit/Checks/CheckBackgroundQuality.cs b/osu.Game/Rulesets/Edit/Checks/CheckBackgroundQuality.cs
index 7ce2ee802e..1f65752fa6 100644
--- a/osu.Game/Rulesets/Edit/Checks/CheckBackgroundQuality.cs
+++ b/osu.Game/Rulesets/Edit/Checks/CheckBackgroundQuality.cs
@@ -48,7 +48,7 @@ namespace osu.Game.Rulesets.Edit.Checks
             else if (texture.Width < low_width || texture.Height < low_height)
                 yield return new IssueTemplateLowResolution(this).Create(texture.Width, texture.Height);
 
-            string storagePath = context.Beatmap.BeatmapInfo.BeatmapSet.GetPathForFile(backgroundFile);
+            string storagePath = context.Beatmap.BeatmapInfo.BeatmapSet?.GetPathForFile(backgroundFile);
 
             using (Stream stream = context.WorkingBeatmap.GetStream(storagePath))
             {
diff --git a/osu.Game/Rulesets/Edit/Checks/CheckFilePresence.cs b/osu.Game/Rulesets/Edit/Checks/CheckFilePresence.cs
index 33bcac1e75..a1605a11d0 100644
--- a/osu.Game/Rulesets/Edit/Checks/CheckFilePresence.cs
+++ b/osu.Game/Rulesets/Edit/Checks/CheckFilePresence.cs
@@ -33,7 +33,7 @@ namespace osu.Game.Rulesets.Edit.Checks
             }
 
             // If the file is set, also make sure it still exists.
-            string storagePath = context.Beatmap.BeatmapInfo.BeatmapSet.GetPathForFile(filename);
+            string storagePath = context.Beatmap.BeatmapInfo.BeatmapSet?.GetPathForFile(filename);
             if (storagePath != null)
                 yield break;
 
diff --git a/osu.Game/Rulesets/Edit/Checks/CheckTooShortAudioFiles.cs b/osu.Game/Rulesets/Edit/Checks/CheckTooShortAudioFiles.cs
index 5cc98c5537..6015d0a1b2 100644
--- a/osu.Game/Rulesets/Edit/Checks/CheckTooShortAudioFiles.cs
+++ b/osu.Game/Rulesets/Edit/Checks/CheckTooShortAudioFiles.cs
@@ -30,32 +30,35 @@ namespace osu.Game.Rulesets.Edit.Checks
         {
             var beatmapSet = context.Beatmap.BeatmapInfo.BeatmapSet;
 
-            foreach (var file in beatmapSet.Files)
+            if (beatmapSet != null)
             {
-                using (Stream data = context.WorkingBeatmap.GetStream(file.FileInfo.GetStoragePath()))
+                foreach (var file in beatmapSet.Files)
                 {
-                    if (data == null)
-                        continue;
-
-                    var fileCallbacks = new FileCallbacks(new DataStreamFileProcedures(data));
-                    int decodeStream = Bass.CreateStream(StreamSystem.NoBuffer, BassFlags.Decode | BassFlags.Prescan, fileCallbacks.Callbacks, fileCallbacks.Handle);
-
-                    if (decodeStream == 0)
+                    using (Stream data = context.WorkingBeatmap.GetStream(file.File.GetStoragePath()))
                     {
-                        // If the file is not likely to be properly parsed by Bass, we don't produce Error issues about it.
-                        // Image files and audio files devoid of audio data both fail, for example, but neither would be issues in this check.
-                        if (hasAudioExtension(file.Filename) && probablyHasAudioData(data))
-                            yield return new IssueTemplateBadFormat(this).Create(file.Filename);
+                        if (data == null)
+                            continue;
 
-                        continue;
+                        var fileCallbacks = new FileCallbacks(new DataStreamFileProcedures(data));
+                        int decodeStream = Bass.CreateStream(StreamSystem.NoBuffer, BassFlags.Decode | BassFlags.Prescan, fileCallbacks.Callbacks, fileCallbacks.Handle);
+
+                        if (decodeStream == 0)
+                        {
+                            // If the file is not likely to be properly parsed by Bass, we don't produce Error issues about it.
+                            // Image files and audio files devoid of audio data both fail, for example, but neither would be issues in this check.
+                            if (hasAudioExtension(file.Filename) && probablyHasAudioData(data))
+                                yield return new IssueTemplateBadFormat(this).Create(file.Filename);
+
+                            continue;
+                        }
+
+                        long length = Bass.ChannelGetLength(decodeStream);
+                        double ms = Bass.ChannelBytes2Seconds(decodeStream, length) * 1000;
+
+                        // Extremely short audio files do not play on some soundcards, resulting in nothing being heard in-game for some users.
+                        if (ms > 0 && ms < ms_threshold)
+                            yield return new IssueTemplateTooShort(this).Create(file.Filename, ms);
                     }
-
-                    long length = Bass.ChannelGetLength(decodeStream);
-                    double ms = Bass.ChannelBytes2Seconds(decodeStream, length) * 1000;
-
-                    // Extremely short audio files do not play on some soundcards, resulting in nothing being heard in-game for some users.
-                    if (ms > 0 && ms < ms_threshold)
-                        yield return new IssueTemplateTooShort(this).Create(file.Filename, ms);
                 }
             }
         }
diff --git a/osu.Game/Rulesets/Edit/Checks/CheckZeroByteFiles.cs b/osu.Game/Rulesets/Edit/Checks/CheckZeroByteFiles.cs
index ab9959aec2..75cb08002f 100644
--- a/osu.Game/Rulesets/Edit/Checks/CheckZeroByteFiles.cs
+++ b/osu.Game/Rulesets/Edit/Checks/CheckZeroByteFiles.cs
@@ -21,12 +21,15 @@ namespace osu.Game.Rulesets.Edit.Checks
         {
             var beatmapSet = context.Beatmap.BeatmapInfo.BeatmapSet;
 
-            foreach (var file in beatmapSet.Files)
+            if (beatmapSet != null)
             {
-                using (Stream data = context.WorkingBeatmap.GetStream(file.FileInfo.GetStoragePath()))
+                foreach (var file in beatmapSet.Files)
                 {
-                    if (data?.Length == 0)
-                        yield return new IssueTemplateZeroBytes(this).Create(file.Filename);
+                    using (Stream data = context.WorkingBeatmap.GetStream(file.File.GetStoragePath()))
+                    {
+                        if (data?.Length == 0)
+                            yield return new IssueTemplateZeroBytes(this).Create(file.Filename);
+                    }
                 }
             }
         }
diff --git a/osu.Game/Rulesets/Mods/DifficultyAdjustSettingsControl.cs b/osu.Game/Rulesets/Mods/DifficultyAdjustSettingsControl.cs
index 67b24d24d0..45873a321a 100644
--- a/osu.Game/Rulesets/Mods/DifficultyAdjustSettingsControl.cs
+++ b/osu.Game/Rulesets/Mods/DifficultyAdjustSettingsControl.cs
@@ -74,10 +74,7 @@ namespace osu.Game.Rulesets.Mods
                 return;
             }
 
-            var difficulty = beatmap.Value.BeatmapInfo.BaseDifficulty;
-
-            if (difficulty == null)
-                return;
+            var difficulty = beatmap.Value.BeatmapInfo.Difficulty;
 
             // generally should always be implemented, else the slider will have a zero default.
             if (difficultyBindable.ReadCurrentFromDifficulty == null)
diff --git a/osu.Game/Rulesets/RulesetInfo.cs b/osu.Game/Rulesets/RulesetInfo.cs
index d018cc4194..2e2ec5c024 100644
--- a/osu.Game/Rulesets/RulesetInfo.cs
+++ b/osu.Game/Rulesets/RulesetInfo.cs
@@ -2,28 +2,81 @@
 // See the LICENCE file in the repository root for full licence text.
 
 using System;
-using System.ComponentModel.DataAnnotations.Schema;
-using System.Diagnostics.CodeAnalysis;
-using Newtonsoft.Json;
+using JetBrains.Annotations;
 using osu.Framework.Testing;
+using Realms;
+
+#nullable enable
 
 namespace osu.Game.Rulesets
 {
     [ExcludeFromDynamicCompile]
-    public sealed class RulesetInfo : IEquatable<RulesetInfo>, IRulesetInfo
+    [MapTo("Ruleset")]
+    public class RulesetInfo : RealmObject, IEquatable<RulesetInfo>, IRulesetInfo
     {
-        public int? ID { get; set; }
+        [PrimaryKey]
+        public string ShortName { get; set; } = string.Empty;
 
-        public string Name { get; set; }
+        [Indexed]
+        public int OnlineID { get; set; } = -1;
 
-        public string ShortName { get; set; }
+        public string Name { get; set; } = string.Empty;
 
-        public string InstantiationInfo { get; set; }
+        public string InstantiationInfo { get; set; } = string.Empty;
+
+        public RulesetInfo(string shortName, string name, string instantiationInfo, int onlineID)
+        {
+            ShortName = shortName;
+            Name = name;
+            InstantiationInfo = instantiationInfo;
+            OnlineID = onlineID;
+        }
+
+        [UsedImplicitly]
+        public RulesetInfo()
+        {
+        }
+
+        public RulesetInfo(int? onlineID, string name, string shortName, bool available)
+        {
+            OnlineID = onlineID ?? -1;
+            Name = name;
+            ShortName = shortName;
+            Available = available;
+        }
 
-        [JsonIgnore]
         public bool Available { get; set; }
 
-        // TODO: this should probably be moved to RulesetStore.
+        public bool Equals(RulesetInfo? other)
+        {
+            if (ReferenceEquals(this, other)) return true;
+            if (other == null) return false;
+
+            return ShortName == other.ShortName;
+        }
+
+        public bool Equals(IRulesetInfo? other) => other is RulesetInfo b && Equals(b);
+
+        public override int GetHashCode()
+        {
+            // Importantly, ignore the underlying realm hash code, as it will usually not match.
+            var hashCode = new HashCode();
+            // ReSharper disable once NonReadonlyMemberInGetHashCode
+            hashCode.Add(ShortName);
+            return hashCode.ToHashCode();
+        }
+
+        public override string ToString() => Name;
+
+        public RulesetInfo Clone() => new RulesetInfo
+        {
+            OnlineID = OnlineID,
+            Name = Name,
+            ShortName = ShortName,
+            InstantiationInfo = InstantiationInfo,
+            Available = Available
+        };
+
         public Ruleset CreateInstance()
         {
             if (!Available)
@@ -40,40 +93,15 @@ namespace osu.Game.Rulesets
                 throw new RulesetLoadException(@"Instantiation failure");
 
             // overwrite the pre-populated RulesetInfo with a potentially database attached copy.
-            ruleset.RulesetInfo = this;
+            // TODO: figure if we still want/need this after switching to realm.
+            // ruleset.RulesetInfo = this;
 
             return ruleset;
         }
 
-        public bool Equals(RulesetInfo other) => other != null && ID == other.ID && Available == other.Available && Name == other.Name && InstantiationInfo == other.InstantiationInfo;
+        #region Compatibility properties
 
-        public override bool Equals(object obj) => obj is RulesetInfo rulesetInfo && Equals(rulesetInfo);
-
-        public bool Equals(IRulesetInfo other) => other is RulesetInfo b && Equals(b);
-
-        [SuppressMessage("ReSharper", "NonReadonlyMemberInGetHashCode")]
-        public override int GetHashCode()
-        {
-            unchecked
-            {
-                int hashCode = ID.HasValue ? ID.GetHashCode() : 0;
-                hashCode = (hashCode * 397) ^ (InstantiationInfo != null ? InstantiationInfo.GetHashCode() : 0);
-                hashCode = (hashCode * 397) ^ (Name != null ? Name.GetHashCode() : 0);
-                hashCode = (hashCode * 397) ^ Available.GetHashCode();
-                return hashCode;
-            }
-        }
-
-        public override string ToString() => Name ?? $"{Name} ({ShortName}) ID: {ID}";
-
-        #region Implementation of IHasOnlineID
-
-        [NotMapped]
-        public int OnlineID
-        {
-            get => ID ?? -1;
-            set => ID = value >= 0 ? value : (int?)null;
-        }
+        public int ID => OnlineID;
 
         #endregion
     }
diff --git a/osu.Game/Rulesets/RulesetStore.cs b/osu.Game/Rulesets/RulesetStore.cs
index 5cc6a75f43..c675fbbf63 100644
--- a/osu.Game/Rulesets/RulesetStore.cs
+++ b/osu.Game/Rulesets/RulesetStore.cs
@@ -7,24 +7,33 @@ using System.IO;
 using System.Linq;
 using System.Reflection;
 using osu.Framework;
+using osu.Framework.Extensions.ObjectExtensions;
 using osu.Framework.Logging;
 using osu.Framework.Platform;
 using osu.Game.Database;
 
+#nullable enable
+
 namespace osu.Game.Rulesets
 {
-    public class RulesetStore : DatabaseBackedStore, IRulesetStore, IDisposable
+    public class RulesetStore : IDisposable, IRulesetStore
     {
-        private const string ruleset_library_prefix = "osu.Game.Rulesets";
+        private readonly RealmContextFactory realmFactory;
+
+        private const string ruleset_library_prefix = @"osu.Game.Rulesets";
 
         private readonly Dictionary<Assembly, Type> loadedAssemblies = new Dictionary<Assembly, Type>();
 
-        private readonly Storage rulesetStorage;
+        /// <summary>
+        /// All available rulesets.
+        /// </summary>
+        public IEnumerable<RulesetInfo> AvailableRulesets => availableRulesets;
 
-        public RulesetStore(IDatabaseContextFactory factory, Storage storage = null)
-            : base(factory)
+        private readonly List<RulesetInfo> availableRulesets = new List<RulesetInfo>();
+
+        public RulesetStore(RealmContextFactory realmFactory, Storage? storage = null)
         {
-            rulesetStorage = storage?.GetStorageForDirectory("rulesets");
+            this.realmFactory = realmFactory;
 
             // On android in release configuration assemblies are loaded from the apk directly into memory.
             // We cannot read assemblies from cwd, so should check loaded assemblies instead.
@@ -40,7 +49,11 @@ namespace osu.Game.Rulesets
             // It needs to be attached to the assembly lookup event before the actual call to loadUserRulesets() else rulesets located out of the base game directory will fail
             // to load as unable to locate the game core assembly.
             AppDomain.CurrentDomain.AssemblyResolve += resolveRulesetDependencyAssembly;
-            loadUserRulesets();
+
+            var rulesetStorage = storage?.GetStorageForDirectory(@"rulesets");
+            if (rulesetStorage != null)
+                loadUserRulesets(rulesetStorage);
+
             addMissingRulesets();
         }
 
@@ -49,21 +62,16 @@ namespace osu.Game.Rulesets
         /// </summary>
         /// <param name="id">The ruleset's internal ID.</param>
         /// <returns>A ruleset, if available, else null.</returns>
-        public RulesetInfo GetRuleset(int id) => AvailableRulesets.FirstOrDefault(r => r.ID == id);
+        public RulesetInfo? GetRuleset(int id) => AvailableRulesets.FirstOrDefault(r => r.OnlineID == id);
 
         /// <summary>
         /// Retrieve a ruleset using a known short name.
         /// </summary>
         /// <param name="shortName">The ruleset's short name.</param>
         /// <returns>A ruleset, if available, else null.</returns>
-        public RulesetInfo GetRuleset(string shortName) => AvailableRulesets.FirstOrDefault(r => r.ShortName == shortName);
+        public RulesetInfo? GetRuleset(string shortName) => AvailableRulesets.FirstOrDefault(r => r.ShortName == shortName);
 
-        /// <summary>
-        /// All available rulesets.
-        /// </summary>
-        public IEnumerable<RulesetInfo> AvailableRulesets { get; private set; }
-
-        private Assembly resolveRulesetDependencyAssembly(object sender, ResolveEventArgs args)
+        private Assembly? resolveRulesetDependencyAssembly(object? sender, ResolveEventArgs args)
         {
             var asm = new AssemblyName(args.Name);
 
@@ -72,7 +80,14 @@ namespace osu.Game.Rulesets
             // already loaded in the AppDomain.
             var domainAssembly = AppDomain.CurrentDomain.GetAssemblies()
                                           // Given name is always going to be equally-or-more qualified than the assembly name.
-                                          .Where(a => args.Name.Contains(a.GetName().Name, StringComparison.Ordinal))
+                                          .Where(a =>
+                                          {
+                                              string? name = a.GetName().Name;
+                                              if (name == null)
+                                                  return false;
+
+                                              return args.Name.Contains(name, StringComparison.Ordinal);
+                                          })
                                           // Pick the greatest assembly version.
                                           .OrderByDescending(a => a.GetName().Version)
                                           .FirstOrDefault();
@@ -85,69 +100,73 @@ namespace osu.Game.Rulesets
 
         private void addMissingRulesets()
         {
-            using (var usage = ContextFactory.GetForWrite())
+            using (var context = realmFactory.CreateContext())
             {
-                var context = usage.Context;
-
-                var instances = loadedAssemblies.Values.Select(r => (Ruleset)Activator.CreateInstance(r)).ToList();
-
-                // add all legacy rulesets first to ensure they have exclusive choice of primary key.
-                foreach (var r in instances.Where(r => r is ILegacyRuleset))
+                context.Write(realm =>
                 {
-                    if (context.RulesetInfo.SingleOrDefault(dbRuleset => dbRuleset.ID == r.RulesetInfo.ID) == null)
-                        context.RulesetInfo.Add(r.RulesetInfo);
-                }
+                    var rulesets = realm.All<RulesetInfo>();
 
-                context.SaveChanges();
+                    List<Ruleset> instances = loadedAssemblies.Values
+                                                              .Select(r => Activator.CreateInstance(r) as Ruleset)
+                                                              .Where(r => r != null)
+                                                              .Select(r => r.AsNonNull())
+                                                              .ToList();
 
-                var existingRulesets = context.RulesetInfo.ToList();
-
-                // add any other rulesets which have assemblies present but are not yet in the database.
-                foreach (var r in instances.Where(r => !(r is ILegacyRuleset)))
-                {
-                    if (existingRulesets.FirstOrDefault(ri => ri.InstantiationInfo.Equals(r.RulesetInfo.InstantiationInfo, StringComparison.Ordinal)) == null)
+                    // add all legacy rulesets first to ensure they have exclusive choice of primary key.
+                    foreach (var r in instances.Where(r => r is ILegacyRuleset))
                     {
-                        var existingSameShortName = existingRulesets.FirstOrDefault(ri => ri.ShortName == r.RulesetInfo.ShortName);
+                        if (realm.All<RulesetInfo>().FirstOrDefault(rr => rr.OnlineID == r.RulesetInfo.OnlineID) == null)
+                            realm.Add(new RulesetInfo(r.RulesetInfo.ShortName, r.RulesetInfo.Name, r.RulesetInfo.InstantiationInfo, r.RulesetInfo.OnlineID));
+                    }
 
-                        if (existingSameShortName != null)
+                    // add any other rulesets which have assemblies present but are not yet in the database.
+                    foreach (var r in instances.Where(r => !(r is ILegacyRuleset)))
+                    {
+                        if (rulesets.FirstOrDefault(ri => ri.InstantiationInfo.Equals(r.RulesetInfo.InstantiationInfo, StringComparison.Ordinal)) == null)
                         {
-                            // even if a matching InstantiationInfo was not found, there may be an existing ruleset with the same ShortName.
-                            // this generally means the user or ruleset provider has renamed their dll but the underlying ruleset is *likely* the same one.
-                            // in such cases, update the instantiation info of the existing entry to point to the new one.
-                            existingSameShortName.InstantiationInfo = r.RulesetInfo.InstantiationInfo;
+                            var existingSameShortName = rulesets.FirstOrDefault(ri => ri.ShortName == r.RulesetInfo.ShortName);
+
+                            if (existingSameShortName != null)
+                            {
+                                // even if a matching InstantiationInfo was not found, there may be an existing ruleset with the same ShortName.
+                                // this generally means the user or ruleset provider has renamed their dll but the underlying ruleset is *likely* the same one.
+                                // in such cases, update the instantiation info of the existing entry to point to the new one.
+                                existingSameShortName.InstantiationInfo = r.RulesetInfo.InstantiationInfo;
+                            }
+                            else
+                                realm.Add(new RulesetInfo(r.RulesetInfo.ShortName, r.RulesetInfo.Name, r.RulesetInfo.InstantiationInfo, r.RulesetInfo.OnlineID));
                         }
-                        else
-                            context.RulesetInfo.Add(r.RulesetInfo);
                     }
-                }
 
-                context.SaveChanges();
+                    List<RulesetInfo> detachedRulesets = new List<RulesetInfo>();
 
-                // perform a consistency check
-                foreach (var r in context.RulesetInfo)
-                {
-                    try
+                    // perform a consistency check and detach final rulesets from realm for cross-thread runtime usage.
+                    foreach (var r in rulesets.OrderBy(r => r.OnlineID))
                     {
-                        var resolvedType = Type.GetType(r.InstantiationInfo)
-                                           ?? throw new RulesetLoadException(@"Type could not be resolved");
+                        try
+                        {
+                            var resolvedType = Type.GetType(r.InstantiationInfo)
+                                               ?? throw new RulesetLoadException(@"Type could not be resolved");
 
-                        var instanceInfo = (Activator.CreateInstance(resolvedType) as Ruleset)?.RulesetInfo
-                                           ?? throw new RulesetLoadException(@"Instantiation failure");
+                            var instanceInfo = (Activator.CreateInstance(resolvedType) as Ruleset)?.RulesetInfo
+                                               ?? throw new RulesetLoadException(@"Instantiation failure");
 
-                        r.Name = instanceInfo.Name;
-                        r.ShortName = instanceInfo.ShortName;
-                        r.InstantiationInfo = instanceInfo.InstantiationInfo;
-                        r.Available = true;
+                            r.Name = instanceInfo.Name;
+                            r.ShortName = instanceInfo.ShortName;
+                            r.InstantiationInfo = instanceInfo.InstantiationInfo;
+                            r.Available = true;
+
+                            detachedRulesets.Add(r.Clone());
+                        }
+                        catch (Exception ex)
+                        {
+                            r.Available = false;
+                            Logger.Log($"Could not load ruleset {r}: {ex.Message}");
+                        }
                     }
-                    catch
-                    {
-                        r.Available = false;
-                    }
-                }
 
-                context.SaveChanges();
-
-                AvailableRulesets = context.RulesetInfo.Where(r => r.Available).ToList();
+                    availableRulesets.AddRange(detachedRulesets);
+                });
             }
         }
 
@@ -155,22 +174,23 @@ namespace osu.Game.Rulesets
         {
             foreach (var ruleset in AppDomain.CurrentDomain.GetAssemblies())
             {
-                string rulesetName = ruleset.GetName().Name;
+                string? rulesetName = ruleset.GetName().Name;
 
-                if (!rulesetName.StartsWith(ruleset_library_prefix, StringComparison.InvariantCultureIgnoreCase) || ruleset.GetName().Name.Contains("Tests"))
+                if (rulesetName == null)
+                    continue;
+
+                if (!rulesetName.StartsWith(ruleset_library_prefix, StringComparison.InvariantCultureIgnoreCase) || rulesetName.Contains(@"Tests"))
                     continue;
 
                 addRuleset(ruleset);
             }
         }
 
-        private void loadUserRulesets()
+        private void loadUserRulesets(Storage rulesetStorage)
         {
-            if (rulesetStorage == null) return;
+            var rulesets = rulesetStorage.GetFiles(@".", @$"{ruleset_library_prefix}.*.dll");
 
-            var rulesets = rulesetStorage.GetFiles(".", $"{ruleset_library_prefix}.*.dll");
-
-            foreach (string ruleset in rulesets.Where(f => !f.Contains("Tests")))
+            foreach (string? ruleset in rulesets.Where(f => !f.Contains(@"Tests")))
                 loadRulesetFromFile(rulesetStorage.GetFullPath(ruleset));
         }
 
@@ -178,7 +198,7 @@ namespace osu.Game.Rulesets
         {
             try
             {
-                string[] files = Directory.GetFiles(RuntimeInfo.StartupDirectory, $"{ruleset_library_prefix}.*.dll");
+                string[] files = Directory.GetFiles(RuntimeInfo.StartupDirectory, @$"{ruleset_library_prefix}.*.dll");
 
                 foreach (string file in files.Where(f => !Path.GetFileName(f).Contains("Tests")))
                     loadRulesetFromFile(file);
@@ -191,7 +211,7 @@ namespace osu.Game.Rulesets
 
         private void loadRulesetFromFile(string file)
         {
-            string filename = Path.GetFileNameWithoutExtension(file);
+            string? filename = Path.GetFileNameWithoutExtension(file);
 
             if (loadedAssemblies.Values.Any(t => Path.GetFileNameWithoutExtension(t.Assembly.Location) == filename))
                 return;
@@ -239,8 +259,8 @@ namespace osu.Game.Rulesets
 
         #region Implementation of IRulesetStore
 
-        IRulesetInfo IRulesetStore.GetRuleset(int id) => GetRuleset(id);
-        IRulesetInfo IRulesetStore.GetRuleset(string shortName) => GetRuleset(shortName);
+        IRulesetInfo? IRulesetStore.GetRuleset(int id) => GetRuleset(id);
+        IRulesetInfo? IRulesetStore.GetRuleset(string shortName) => GetRuleset(shortName);
         IEnumerable<IRulesetInfo> IRulesetStore.AvailableRulesets => AvailableRulesets;
 
         #endregion
diff --git a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs
index c0b339a231..f2dbb1a23f 100644
--- a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs
+++ b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs
@@ -61,7 +61,7 @@ namespace osu.Game.Rulesets.UI
         private int direction = 1;
 
         [BackgroundDependencyLoader(true)]
-        private void load(GameplayClock clock, ISamplePlaybackDisabler sampleDisabler)
+        private void load(GameplayClock clock)
         {
             if (clock != null)
             {
diff --git a/osu.Game/Scoring/EFScoreInfo.cs b/osu.Game/Scoring/EFScoreInfo.cs
new file mode 100644
index 0000000000..1dd4e3b6b3
--- /dev/null
+++ b/osu.Game/Scoring/EFScoreInfo.cs
@@ -0,0 +1,270 @@
+// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations.Schema;
+using System.Linq;
+using Newtonsoft.Json;
+using osu.Framework.Localisation;
+using osu.Game.Beatmaps;
+using osu.Game.Database;
+using osu.Game.Online.API;
+using osu.Game.Online.API.Requests.Responses;
+using osu.Game.Rulesets;
+using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Scoring;
+using osu.Game.Users;
+using osu.Game.Utils;
+
+namespace osu.Game.Scoring
+{
+    [Table(@"ScoreInfo")]
+    public class EFScoreInfo : IScoreInfo, IHasFiles<ScoreFileInfo>, IHasPrimaryKey, ISoftDelete, IEquatable<EFScoreInfo>, IDeepCloneable<EFScoreInfo>
+    {
+        public int ID { get; set; }
+
+        public bool IsManaged => ID > 0;
+
+        public ScoreRank Rank { get; set; }
+
+        public long TotalScore { get; set; }
+
+        [Column(TypeName = "DECIMAL(1,4)")] // TODO: This data type is wrong (should contain more precision). But at the same time, we probably don't need to be storing this in the database.
+        public double Accuracy { get; set; }
+
+        public LocalisableString DisplayAccuracy => Accuracy.FormatAccuracy();
+
+        public double? PP { get; set; }
+
+        public int MaxCombo { get; set; }
+
+        public int Combo { get; set; } // Todo: Shouldn't exist in here
+
+        public int RulesetID { get; set; }
+
+        [NotMapped]
+        public bool Passed { get; set; } = true;
+
+        public EFRulesetInfo Ruleset { get; set; }
+
+        private APIMod[] localAPIMods;
+
+        private Mod[] mods;
+
+        [NotMapped]
+        public Mod[] Mods
+        {
+            get
+            {
+                var rulesetInstance = Ruleset?.CreateInstance();
+                if (rulesetInstance == null)
+                    return mods ?? Array.Empty<Mod>();
+
+                Mod[] scoreMods = Array.Empty<Mod>();
+
+                if (mods != null)
+                    scoreMods = mods;
+                else if (localAPIMods != null)
+                    scoreMods = APIMods.Select(m => m.ToMod(rulesetInstance)).ToArray();
+
+                return scoreMods;
+            }
+            set
+            {
+                localAPIMods = null;
+                mods = value;
+            }
+        }
+
+        // Used for API serialisation/deserialisation.
+        [NotMapped]
+        public APIMod[] APIMods
+        {
+            get
+            {
+                if (localAPIMods != null)
+                    return localAPIMods;
+
+                if (mods == null)
+                    return Array.Empty<APIMod>();
+
+                return localAPIMods = mods.Select(m => new APIMod(m)).ToArray();
+            }
+            set
+            {
+                localAPIMods = value;
+
+                // We potentially can't update this yet due to Ruleset being late-bound, so instead update on read as necessary.
+                mods = null;
+            }
+        }
+
+        // Used for database serialisation/deserialisation.
+        [Column("Mods")]
+        public string ModsJson
+        {
+            get => JsonConvert.SerializeObject(APIMods);
+            set => APIMods = JsonConvert.DeserializeObject<APIMod[]>(value);
+        }
+
+        [NotMapped]
+        public APIUser User { get; set; }
+
+        [Column("User")]
+        public string UserString
+        {
+            get => User?.Username;
+            set
+            {
+                User ??= new APIUser();
+                User.Username = value;
+            }
+        }
+
+        [Column("UserID")]
+        public int? UserID
+        {
+            get => User?.Id ?? 1;
+            set
+            {
+                User ??= new APIUser();
+                User.Id = value ?? 1;
+            }
+        }
+
+        public int BeatmapInfoID { get; set; }
+
+        [Column("Beatmap")]
+        public EFBeatmapInfo BeatmapInfo { get; set; }
+
+        private long? onlineID;
+
+        [JsonProperty("id")]
+        [Column("OnlineScoreID")]
+        public long? OnlineID
+        {
+            get => onlineID;
+            set => onlineID = value > 0 ? value : null;
+        }
+
+        public DateTimeOffset Date { get; set; }
+
+        [NotMapped]
+        public Dictionary<HitResult, int> Statistics { get; set; } = new Dictionary<HitResult, int>();
+
+        [Column("Statistics")]
+        public string StatisticsJson
+        {
+            get => JsonConvert.SerializeObject(Statistics);
+            set
+            {
+                if (value == null)
+                {
+                    Statistics.Clear();
+                    return;
+                }
+
+                Statistics = JsonConvert.DeserializeObject<Dictionary<HitResult, int>>(value);
+            }
+        }
+
+        [NotMapped]
+        public List<HitEvent> HitEvents { get; set; }
+
+        public List<ScoreFileInfo> Files { get; } = new List<ScoreFileInfo>();
+
+        public string Hash { get; set; }
+
+        public bool DeletePending { get; set; }
+
+        /// <summary>
+        /// The position of this score, starting at 1.
+        /// </summary>
+        [NotMapped]
+        public int? Position { get; set; } // TODO: remove after all calls to `CreateScoreInfo` are gone.
+
+        /// <summary>
+        /// Whether this <see cref="EFScoreInfo"/> represents a legacy (osu!stable) score.
+        /// </summary>
+        [NotMapped]
+        public bool IsLegacyScore => Mods.OfType<ModClassic>().Any();
+
+        public IEnumerable<HitResultDisplayStatistic> GetStatisticsForDisplay()
+        {
+            foreach (var r in Ruleset.CreateInstance().GetHitResults())
+            {
+                int value = Statistics.GetValueOrDefault(r.result);
+
+                switch (r.result)
+                {
+                    case HitResult.SmallTickHit:
+                    {
+                        int total = value + Statistics.GetValueOrDefault(HitResult.SmallTickMiss);
+                        if (total > 0)
+                            yield return new HitResultDisplayStatistic(r.result, value, total, r.displayName);
+
+                        break;
+                    }
+
+                    case HitResult.LargeTickHit:
+                    {
+                        int total = value + Statistics.GetValueOrDefault(HitResult.LargeTickMiss);
+                        if (total > 0)
+                            yield return new HitResultDisplayStatistic(r.result, value, total, r.displayName);
+
+                        break;
+                    }
+
+                    case HitResult.SmallTickMiss:
+                    case HitResult.LargeTickMiss:
+                        break;
+
+                    default:
+                        yield return new HitResultDisplayStatistic(r.result, value, null, r.displayName);
+
+                        break;
+                }
+            }
+        }
+
+        public EFScoreInfo DeepClone()
+        {
+            var clone = (EFScoreInfo)MemberwiseClone();
+
+            clone.Statistics = new Dictionary<HitResult, int>(clone.Statistics);
+
+            return clone;
+        }
+
+        public override string ToString() => this.GetDisplayTitle();
+
+        public bool Equals(EFScoreInfo other)
+        {
+            if (ReferenceEquals(this, other)) return true;
+            if (other == null) return false;
+
+            if (ID != 0 && other.ID != 0)
+                return ID == other.ID;
+
+            return false;
+        }
+
+        #region Implementation of IHasOnlineID
+
+        long IHasOnlineID<long>.OnlineID => OnlineID ?? -1;
+
+        #endregion
+
+        #region Implementation of IScoreInfo
+
+        IBeatmapInfo IScoreInfo.Beatmap => BeatmapInfo;
+        IRulesetInfo IScoreInfo.Ruleset => Ruleset;
+        IUser IScoreInfo.User => User;
+        bool IScoreInfo.HasReplay => Files.Any();
+
+        #endregion
+
+        IEnumerable<INamedFileUsage> IHasNamedFiles.Files => Files;
+    }
+}
diff --git a/osu.Game/Scoring/Legacy/DatabasedLegacyScoreDecoder.cs b/osu.Game/Scoring/Legacy/DatabasedLegacyScoreDecoder.cs
index 9b590f56dd..03e13455f0 100644
--- a/osu.Game/Scoring/Legacy/DatabasedLegacyScoreDecoder.cs
+++ b/osu.Game/Scoring/Legacy/DatabasedLegacyScoreDecoder.cs
@@ -12,16 +12,16 @@ namespace osu.Game.Scoring.Legacy
     /// </summary>
     public class DatabasedLegacyScoreDecoder : LegacyScoreDecoder
     {
-        private readonly RulesetStore rulesets;
+        private readonly IRulesetStore rulesets;
         private readonly BeatmapManager beatmaps;
 
-        public DatabasedLegacyScoreDecoder(RulesetStore rulesets, BeatmapManager beatmaps)
+        public DatabasedLegacyScoreDecoder(IRulesetStore rulesets, BeatmapManager beatmaps)
         {
             this.rulesets = rulesets;
             this.beatmaps = beatmaps;
         }
 
-        protected override Ruleset GetRuleset(int rulesetId) => rulesets.GetRuleset(rulesetId).CreateInstance();
-        protected override WorkingBeatmap GetBeatmap(string md5Hash) => beatmaps.GetWorkingBeatmap(beatmaps.QueryBeatmap(b => !b.BeatmapSet.DeletePending && b.MD5Hash == md5Hash));
+        protected override Ruleset GetRuleset(int rulesetId) => rulesets.GetRuleset(rulesetId)?.CreateInstance();
+        protected override WorkingBeatmap GetBeatmap(string md5Hash) => beatmaps.GetWorkingBeatmap(beatmaps.QueryBeatmap(b => b.MD5Hash == md5Hash));
     }
 }
diff --git a/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs
index 3d67aa9558..2902ff7848 100644
--- a/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs
+++ b/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs
@@ -45,8 +45,8 @@ namespace osu.Game.Scoring.Legacy
                 sw.Write((byte)(score.ScoreInfo.Ruleset.OnlineID));
                 sw.Write(LATEST_VERSION);
                 sw.Write(score.ScoreInfo.BeatmapInfo.MD5Hash);
-                sw.Write(score.ScoreInfo.UserString);
-                sw.Write(FormattableString.Invariant($"lazer-{score.ScoreInfo.UserString}-{score.ScoreInfo.Date}").ComputeMD5Hash());
+                sw.Write(score.ScoreInfo.User.Username);
+                sw.Write(FormattableString.Invariant($"lazer-{score.ScoreInfo.User.Username}-{score.ScoreInfo.Date}").ComputeMD5Hash());
                 sw.Write((ushort)(score.ScoreInfo.GetCount300() ?? 0));
                 sw.Write((ushort)(score.ScoreInfo.GetCount100() ?? 0));
                 sw.Write((ushort)(score.ScoreInfo.GetCount50() ?? 0));
diff --git a/osu.Game/Scoring/Legacy/ScoreInfoExtensions.cs b/osu.Game/Scoring/Legacy/ScoreInfoExtensions.cs
index fc27261225..a11cd5fcbd 100644
--- a/osu.Game/Scoring/Legacy/ScoreInfoExtensions.cs
+++ b/osu.Game/Scoring/Legacy/ScoreInfoExtensions.cs
@@ -9,7 +9,7 @@ namespace osu.Game.Scoring.Legacy
     {
         public static int? GetCountGeki(this ScoreInfo scoreInfo)
         {
-            switch (scoreInfo.Ruleset?.ID ?? scoreInfo.RulesetID)
+            switch (scoreInfo.Ruleset.OnlineID)
             {
                 case 3:
                     return getCount(scoreInfo, HitResult.Perfect);
@@ -20,7 +20,7 @@ namespace osu.Game.Scoring.Legacy
 
         public static void SetCountGeki(this ScoreInfo scoreInfo, int value)
         {
-            switch (scoreInfo.Ruleset?.ID ?? scoreInfo.RulesetID)
+            switch (scoreInfo.Ruleset.OnlineID)
             {
                 case 3:
                     scoreInfo.Statistics[HitResult.Perfect] = value;
@@ -34,7 +34,7 @@ namespace osu.Game.Scoring.Legacy
 
         public static int? GetCountKatu(this ScoreInfo scoreInfo)
         {
-            switch (scoreInfo.Ruleset?.ID ?? scoreInfo.RulesetID)
+            switch (scoreInfo.Ruleset.OnlineID)
             {
                 case 3:
                     return getCount(scoreInfo, HitResult.Good);
@@ -48,7 +48,7 @@ namespace osu.Game.Scoring.Legacy
 
         public static void SetCountKatu(this ScoreInfo scoreInfo, int value)
         {
-            switch (scoreInfo.Ruleset?.ID ?? scoreInfo.RulesetID)
+            switch (scoreInfo.Ruleset.OnlineID)
             {
                 case 3:
                     scoreInfo.Statistics[HitResult.Good] = value;
@@ -62,7 +62,7 @@ namespace osu.Game.Scoring.Legacy
 
         public static int? GetCount100(this ScoreInfo scoreInfo)
         {
-            switch (scoreInfo.Ruleset?.ID ?? scoreInfo.RulesetID)
+            switch (scoreInfo.Ruleset.OnlineID)
             {
                 case 0:
                 case 1:
@@ -78,7 +78,7 @@ namespace osu.Game.Scoring.Legacy
 
         public static void SetCount100(this ScoreInfo scoreInfo, int value)
         {
-            switch (scoreInfo.Ruleset?.ID ?? scoreInfo.RulesetID)
+            switch (scoreInfo.Ruleset.OnlineID)
             {
                 case 0:
                 case 1:
@@ -94,7 +94,7 @@ namespace osu.Game.Scoring.Legacy
 
         public static int? GetCount50(this ScoreInfo scoreInfo)
         {
-            switch (scoreInfo.Ruleset?.ID ?? scoreInfo.RulesetID)
+            switch (scoreInfo.Ruleset.OnlineID)
             {
                 case 0:
                 case 3:
@@ -109,7 +109,7 @@ namespace osu.Game.Scoring.Legacy
 
         public static void SetCount50(this ScoreInfo scoreInfo, int value)
         {
-            switch (scoreInfo.Ruleset?.ID ?? scoreInfo.RulesetID)
+            switch (scoreInfo.Ruleset.OnlineID)
             {
                 case 0:
                 case 3:
diff --git a/osu.Game/Scoring/LegacyDatabasedScore.cs b/osu.Game/Scoring/LegacyDatabasedScore.cs
index 69360cacc7..ac444c1bf3 100644
--- a/osu.Game/Scoring/LegacyDatabasedScore.cs
+++ b/osu.Game/Scoring/LegacyDatabasedScore.cs
@@ -17,7 +17,7 @@ namespace osu.Game.Scoring
         {
             ScoreInfo = score;
 
-            string replayFilename = score.Files.FirstOrDefault(f => f.Filename.EndsWith(".osr", StringComparison.InvariantCultureIgnoreCase))?.FileInfo.GetStoragePath();
+            string replayFilename = score.Files.FirstOrDefault(f => f.Filename.EndsWith(".osr", StringComparison.InvariantCultureIgnoreCase))?.File.GetStoragePath();
 
             if (replayFilename == null)
                 return;
diff --git a/osu.Game/Scoring/ScoreFileInfo.cs b/osu.Game/Scoring/ScoreFileInfo.cs
index 4c88cfa021..8acc98eff6 100644
--- a/osu.Game/Scoring/ScoreFileInfo.cs
+++ b/osu.Game/Scoring/ScoreFileInfo.cs
@@ -13,6 +13,10 @@ namespace osu.Game.Scoring
 
         public bool IsManaged => ID > 0;
 
+        public int ScoreInfoID { get; set; }
+
+        public EFScoreInfo ScoreInfo { get; set; }
+
         public int FileInfoID { get; set; }
 
         public FileInfo FileInfo { get; set; }
diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs
index 7acc7bd055..b268e2cd91 100644
--- a/osu.Game/Scoring/ScoreInfo.cs
+++ b/osu.Game/Scoring/ScoreInfo.cs
@@ -3,12 +3,14 @@
 
 using System;
 using System.Collections.Generic;
-using System.ComponentModel.DataAnnotations.Schema;
 using System.Linq;
+using JetBrains.Annotations;
 using Newtonsoft.Json;
 using osu.Framework.Localisation;
+using osu.Framework.Testing;
 using osu.Game.Beatmaps;
 using osu.Game.Database;
+using osu.Game.Models;
 using osu.Game.Online.API;
 using osu.Game.Online.API.Requests.Responses;
 using osu.Game.Rulesets;
@@ -16,178 +18,221 @@ using osu.Game.Rulesets.Mods;
 using osu.Game.Rulesets.Scoring;
 using osu.Game.Users;
 using osu.Game.Utils;
+using Realms;
+
+#nullable enable
 
 namespace osu.Game.Scoring
 {
-    public class ScoreInfo : IScoreInfo, IHasFiles<ScoreFileInfo>, IHasPrimaryKey, ISoftDelete, IEquatable<ScoreInfo>, IDeepCloneable<ScoreInfo>
+    [ExcludeFromDynamicCompile]
+    [MapTo("Score")]
+    public class ScoreInfo : RealmObject, IHasGuidPrimaryKey, IHasRealmFiles, ISoftDelete, IEquatable<ScoreInfo>, IScoreInfo
     {
-        public int ID { get; set; }
+        [PrimaryKey]
+        public Guid ID { get; set; } = Guid.NewGuid();
 
-        public bool IsManaged => ID > 0;
-
-        public ScoreRank Rank { get; set; }
-
-        public long TotalScore { get; set; }
-
-        [Column(TypeName = "DECIMAL(1,4)")] // TODO: This data type is wrong (should contain more precision). But at the same time, we probably don't need to be storing this in the database.
-        public double Accuracy { get; set; }
-
-        public LocalisableString DisplayAccuracy => Accuracy.FormatAccuracy();
-
-        public double? PP { get; set; }
-
-        public int MaxCombo { get; set; }
-
-        public int Combo { get; set; } // Todo: Shouldn't exist in here
-
-        public int RulesetID { get; set; }
-
-        [NotMapped]
-        public bool Passed { get; set; } = true;
+        public BeatmapInfo BeatmapInfo { get; set; }
 
         public RulesetInfo Ruleset { get; set; }
 
-        private APIMod[] localAPIMods;
+        public IList<RealmNamedFileUsage> Files { get; } = null!;
 
-        private Mod[] mods;
+        public string Hash { get; set; } = string.Empty;
 
-        [NotMapped]
-        public Mod[] Mods
-        {
-            get
-            {
-                var rulesetInstance = Ruleset?.CreateInstance();
-                if (rulesetInstance == null)
-                    return mods ?? Array.Empty<Mod>();
+        public bool DeletePending { get; set; }
 
-                Mod[] scoreMods = Array.Empty<Mod>();
+        public long TotalScore { get; set; }
 
-                if (mods != null)
-                    scoreMods = mods;
-                else if (localAPIMods != null)
-                    scoreMods = APIMods.Select(m => m.ToMod(rulesetInstance)).ToArray();
+        public int MaxCombo { get; set; }
 
-                return scoreMods;
-            }
-            set
-            {
-                localAPIMods = null;
-                mods = value;
-            }
-        }
+        public double Accuracy { get; set; }
 
-        // Used for API serialisation/deserialisation.
-        [NotMapped]
-        public APIMod[] APIMods
-        {
-            get
-            {
-                if (localAPIMods != null)
-                    return localAPIMods;
-
-                if (mods == null)
-                    return Array.Empty<APIMod>();
-
-                return localAPIMods = mods.Select(m => new APIMod(m)).ToArray();
-            }
-            set
-            {
-                localAPIMods = value;
-
-                // We potentially can't update this yet due to Ruleset being late-bound, so instead update on read as necessary.
-                mods = null;
-            }
-        }
-
-        // Used for database serialisation/deserialisation.
-        [Column("Mods")]
-        public string ModsJson
-        {
-            get => JsonConvert.SerializeObject(APIMods);
-            set => APIMods = JsonConvert.DeserializeObject<APIMod[]>(value);
-        }
-
-        [NotMapped]
-        public APIUser User { get; set; }
-
-        [Column("User")]
-        public string UserString
-        {
-            get => User?.Username;
-            set
-            {
-                User ??= new APIUser();
-                User.Username = value;
-            }
-        }
-
-        [Column("UserID")]
-        public int? UserID
-        {
-            get => User?.Id ?? 1;
-            set
-            {
-                User ??= new APIUser();
-                User.Id = value ?? 1;
-            }
-        }
-
-        public int BeatmapInfoID { get; set; }
-
-        [Column("Beatmap")]
-        public BeatmapInfo BeatmapInfo { get; set; }
-
-        private long? onlineID;
-
-        [Column("OnlineScoreID")]
-        public long? OnlineID
-        {
-            get => onlineID;
-            set => onlineID = value > 0 ? value : null;
-        }
+        public bool HasReplay { get; set; }
 
         public DateTimeOffset Date { get; set; }
 
-        [NotMapped]
-        public Dictionary<HitResult, int> Statistics { get; set; } = new Dictionary<HitResult, int>();
+        public double? PP { get; set; }
 
-        [Column("Statistics")]
-        public string StatisticsJson
+        [Indexed]
+        public long OnlineID { get; set; } = -1;
+
+        [MapTo("User")]
+        public RealmUser RealmUser { get; set; }
+
+        [MapTo("Mods")]
+        public string ModsJson { get; set; } = string.Empty;
+
+        [MapTo("Statistics")]
+        public string StatisticsJson { get; set; } = string.Empty;
+
+        public ScoreInfo(BeatmapInfo beatmap, RulesetInfo ruleset, RealmUser realmUser)
         {
-            get => JsonConvert.SerializeObject(Statistics);
+            Ruleset = ruleset;
+            BeatmapInfo = beatmap;
+            RealmUser = realmUser;
+        }
+
+        [UsedImplicitly]
+        public ScoreInfo() // TODO: consider removing this and migrating all usages to ctor with parameters.
+        {
+            Ruleset = new RulesetInfo();
+            RealmUser = new RealmUser();
+            BeatmapInfo = new BeatmapInfo();
+        }
+
+        // TODO: this is a bit temporary to account for the fact that this class is used to ferry API user data to certain UI components.
+        // Eventually we should either persist enough information to realm to not require the API lookups, or perform the API lookups locally.
+        private APIUser? user;
+
+        [Ignored]
+        public APIUser User
+        {
+            get => user ??= new APIUser
+            {
+                Username = RealmUser.Username,
+                Id = RealmUser.OnlineID,
+            };
             set
             {
-                if (value == null)
-                {
-                    Statistics.Clear();
-                    return;
-                }
+                user = value;
 
-                Statistics = JsonConvert.DeserializeObject<Dictionary<HitResult, int>>(value);
+                RealmUser = new RealmUser
+                {
+                    OnlineID = user.OnlineID,
+                    Username = user.Username
+                };
             }
         }
 
-        [NotMapped]
-        public List<HitEvent> HitEvents { get; set; }
+        [Ignored]
+        public ScoreRank Rank
+        {
+            get => (ScoreRank)RankInt;
+            set => RankInt = (int)value;
+        }
 
-        public List<ScoreFileInfo> Files { get; } = new List<ScoreFileInfo>();
+        [MapTo(nameof(Rank))]
+        public int RankInt { get; set; }
 
-        public string Hash { get; set; }
+        IRulesetInfo IScoreInfo.Ruleset => Ruleset;
+        IBeatmapInfo IScoreInfo.Beatmap => BeatmapInfo;
+        IUser IScoreInfo.User => User;
+        IEnumerable<INamedFileUsage> IHasNamedFiles.Files => Files;
 
-        public bool DeletePending { get; set; }
+        #region Properties required to make things work with existing usages
+
+        public Guid BeatmapInfoID => BeatmapInfo.ID;
+
+        public int UserID => RealmUser.OnlineID;
+
+        public int RulesetID => Ruleset.OnlineID;
+
+        [Ignored]
+        public List<HitEvent> HitEvents { get; set; } = new List<HitEvent>();
+
+        public ScoreInfo DeepClone()
+        {
+            var clone = (ScoreInfo)this.Detach().MemberwiseClone();
+
+            clone.Statistics = new Dictionary<HitResult, int>(clone.Statistics);
+
+            return clone;
+        }
+
+        [Ignored]
+        public bool Passed { get; set; } = true;
+
+        public int Combo { get; set; }
 
         /// <summary>
         /// The position of this score, starting at 1.
         /// </summary>
-        [NotMapped]
+        [Ignored]
         public int? Position { get; set; } // TODO: remove after all calls to `CreateScoreInfo` are gone.
 
+        [Ignored]
+        public LocalisableString DisplayAccuracy => Accuracy.FormatAccuracy();
+
         /// <summary>
-        /// Whether this <see cref="ScoreInfo"/> represents a legacy (osu!stable) score.
+        /// Whether this <see cref="EFScoreInfo"/> represents a legacy (osu!stable) score.
         /// </summary>
-        [NotMapped]
+        [Ignored]
         public bool IsLegacyScore => Mods.OfType<ModClassic>().Any();
 
+        private Dictionary<HitResult, int>? statistics;
+
+        [Ignored]
+        public Dictionary<HitResult, int> Statistics
+        {
+            get
+            {
+                if (statistics != null)
+                    return statistics;
+
+                if (!string.IsNullOrEmpty(StatisticsJson))
+                    statistics = JsonConvert.DeserializeObject<Dictionary<HitResult, int>>(StatisticsJson);
+
+                return statistics ??= new Dictionary<HitResult, int>();
+            }
+            set => statistics = value;
+        }
+
+        private Mod[]? mods;
+
+        [Ignored]
+        public Mod[] Mods
+        {
+            get
+            {
+                if (mods != null)
+                    return mods;
+
+                return APIMods.Select(m => m.ToMod(Ruleset.CreateInstance())).ToArray();
+            }
+            set
+            {
+                apiMods = null;
+                mods = value;
+
+                updateModsJson();
+            }
+        }
+
+        private APIMod[]? apiMods;
+
+        // Used for API serialisation/deserialisation.
+        [Ignored]
+        public APIMod[] APIMods
+        {
+            get
+            {
+                if (apiMods != null) return apiMods;
+
+                // prioritise reading from realm backing
+                if (!string.IsNullOrEmpty(ModsJson))
+                    apiMods = JsonConvert.DeserializeObject<APIMod[]>(ModsJson);
+
+                // then check mods set via Mods property.
+                if (mods != null)
+                    apiMods ??= mods.Select(m => new APIMod(m)).ToArray();
+
+                return apiMods ?? Array.Empty<APIMod>();
+            }
+            set
+            {
+                apiMods = value;
+                mods = null;
+
+                // We potentially can't update this yet due to Ruleset being late-bound, so instead update on read as necessary.
+                updateModsJson();
+            }
+        }
+
+        private void updateModsJson()
+        {
+            ModsJson = JsonConvert.SerializeObject(APIMods);
+        }
+
         public IEnumerable<HitResultDisplayStatistic> GetStatisticsForDisplay()
         {
             foreach (var r in Ruleset.CreateInstance().GetHitResults())
@@ -226,43 +271,10 @@ namespace osu.Game.Scoring
             }
         }
 
-        public ScoreInfo DeepClone()
-        {
-            var clone = (ScoreInfo)MemberwiseClone();
+        #endregion
 
-            clone.Statistics = new Dictionary<HitResult, int>(clone.Statistics);
-
-            return clone;
-        }
+        public bool Equals(ScoreInfo other) => other.ID == ID;
 
         public override string ToString() => this.GetDisplayTitle();
-
-        public bool Equals(ScoreInfo other)
-        {
-            if (ReferenceEquals(this, other)) return true;
-            if (other == null) return false;
-
-            if (ID != 0 && other.ID != 0)
-                return ID == other.ID;
-
-            return false;
-        }
-
-        #region Implementation of IHasOnlineID
-
-        long IHasOnlineID<long>.OnlineID => OnlineID ?? -1;
-
-        #endregion
-
-        #region Implementation of IScoreInfo
-
-        IBeatmapInfo IScoreInfo.Beatmap => BeatmapInfo;
-        IRulesetInfo IScoreInfo.Ruleset => Ruleset;
-        IUser IScoreInfo.User => User;
-        bool IScoreInfo.HasReplay => Files.Any();
-
-        #endregion
-
-        IEnumerable<INamedFileUsage> IHasNamedFiles.Files => Files;
     }
 }
diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs
index 39cd28cad2..ccf3226792 100644
--- a/osu.Game/Scoring/ScoreManager.cs
+++ b/osu.Game/Scoring/ScoreManager.cs
@@ -25,28 +25,35 @@ namespace osu.Game.Scoring
 {
     public class ScoreManager : IModelManager<ScoreInfo>, IModelImporter<ScoreInfo>
     {
+        private readonly RealmContextFactory contextFactory;
         private readonly Scheduler scheduler;
         private readonly Func<BeatmapDifficultyCache> difficulties;
         private readonly OsuConfigManager configManager;
         private readonly ScoreModelManager scoreModelManager;
 
-        public ScoreManager(RulesetStore rulesets, Func<BeatmapManager> beatmaps, Storage storage, IDatabaseContextFactory contextFactory, Scheduler scheduler,
+        public ScoreManager(RulesetStore rulesets, Func<BeatmapManager> beatmaps, Storage storage, RealmContextFactory contextFactory, Scheduler scheduler,
                             IIpcHost importHost = null, Func<BeatmapDifficultyCache> difficulties = null, OsuConfigManager configManager = null)
         {
+            this.contextFactory = contextFactory;
             this.scheduler = scheduler;
             this.difficulties = difficulties;
             this.configManager = configManager;
 
-            scoreModelManager = new ScoreModelManager(rulesets, beatmaps, storage, contextFactory, importHost);
+            scoreModelManager = new ScoreModelManager(rulesets, beatmaps, storage, contextFactory);
         }
 
         public Score GetScore(ScoreInfo score) => scoreModelManager.GetScore(score);
 
-        public List<ScoreInfo> GetAllUsableScores() => scoreModelManager.GetAllUsableScores();
-
-        public IEnumerable<ScoreInfo> QueryScores(Expression<Func<ScoreInfo, bool>> query) => scoreModelManager.QueryScores(query);
-
-        public ScoreInfo Query(Expression<Func<ScoreInfo, bool>> query) => scoreModelManager.Query(query);
+        /// <summary>
+        /// Perform a lookup query on available <see cref="ScoreInfo"/>s.
+        /// </summary>
+        /// <param name="query">The query.</param>
+        /// <returns>The first result for the provided query, or null if no results were found.</returns>
+        public ScoreInfo Query(Expression<Func<ScoreInfo, bool>> query)
+        {
+            using (var context = contextFactory.CreateContext())
+                return context.All<ScoreInfo>().FirstOrDefault(query)?.Detach();
+        }
 
         /// <summary>
         /// Orders an array of <see cref="ScoreInfo"/>s by total score.
@@ -125,7 +132,8 @@ namespace osu.Game.Scoring
         /// <returns>The total score.</returns>
         public async Task<long> GetTotalScoreAsync([NotNull] ScoreInfo score, ScoringMode mode = ScoringMode.Standardised, CancellationToken cancellationToken = default)
         {
-            if (score.BeatmapInfo == null)
+            // TODO: This is required for playlist aggregate scores. They should likely not be getting here in the first place.
+            if (string.IsNullOrEmpty(score.BeatmapInfo.Hash))
                 return score.TotalScore;
 
             int beatmapMaxCombo;
@@ -150,11 +158,8 @@ namespace osu.Game.Scoring
                     beatmapMaxCombo = score.BeatmapInfo.MaxCombo.Value;
                 else
                 {
-                    if (score.BeatmapInfo.ID == 0 || difficulties == null)
-                    {
-                        // We don't have enough information (max combo) to compute the score, so use the provided score.
+                    if (difficulties == null)
                         return score.TotalScore;
-                    }
 
                     // We can compute the max combo locally after the async beatmap difficulty computation.
                     var difficulty = await difficulties().GetDifficultyAsync(score.BeatmapInfo, score.Ruleset, score.Mods, cancellationToken).ConfigureAwait(false);
@@ -243,28 +248,25 @@ namespace osu.Game.Scoring
 
         #region Implementation of IModelManager<ScoreInfo>
 
-        public event Action<ScoreInfo> ItemUpdated
-        {
-            add => scoreModelManager.ItemUpdated += value;
-            remove => scoreModelManager.ItemUpdated -= value;
-        }
-
-        public event Action<ScoreInfo> ItemRemoved
-        {
-            add => scoreModelManager.ItemRemoved += value;
-            remove => scoreModelManager.ItemRemoved -= value;
-        }
-
-        public void Update(ScoreInfo item)
-        {
-            scoreModelManager.Update(item);
-        }
-
         public bool Delete(ScoreInfo item)
         {
             return scoreModelManager.Delete(item);
         }
 
+        public void Delete([CanBeNull] Expression<Func<ScoreInfo, bool>> filter = null, bool silent = false)
+        {
+            using (var context = contextFactory.CreateContext())
+            {
+                var items = context.All<ScoreInfo>()
+                                   .Where(s => !s.DeletePending);
+
+                if (filter != null)
+                    items = items.Where(filter);
+
+                scoreModelManager.Delete(items.ToList(), silent);
+            }
+        }
+
         public void Delete(List<ScoreInfo> items, bool silent = false)
         {
             scoreModelManager.Delete(items, silent);
diff --git a/osu.Game/Scoring/ScoreModelManager.cs b/osu.Game/Scoring/ScoreModelManager.cs
index 44f0fe4fdf..5ba152fad3 100644
--- a/osu.Game/Scoring/ScoreModelManager.cs
+++ b/osu.Game/Scoring/ScoreModelManager.cs
@@ -4,10 +4,8 @@
 using System;
 using System.Collections.Generic;
 using System.Linq;
-using System.Linq.Expressions;
 using System.Threading;
-using System.Threading.Tasks;
-using Microsoft.EntityFrameworkCore;
+using Newtonsoft.Json;
 using osu.Framework.Logging;
 using osu.Framework.Platform;
 using osu.Game.Beatmaps;
@@ -15,10 +13,14 @@ using osu.Game.Database;
 using osu.Game.IO.Archives;
 using osu.Game.Rulesets;
 using osu.Game.Scoring.Legacy;
+using osu.Game.Stores;
+using Realms;
+
+#nullable enable
 
 namespace osu.Game.Scoring
 {
-    public class ScoreModelManager : ArchiveModelManager<ScoreInfo, ScoreFileInfo>
+    public class ScoreModelManager : RealmArchiveModelManager<ScoreInfo>
     {
         public override IEnumerable<string> HandledExtensions => new[] { ".osr" };
 
@@ -27,18 +29,15 @@ namespace osu.Game.Scoring
         private readonly RulesetStore rulesets;
         private readonly Func<BeatmapManager> beatmaps;
 
-        public ScoreModelManager(RulesetStore rulesets, Func<BeatmapManager> beatmaps, Storage storage, IDatabaseContextFactory contextFactory, IIpcHost importHost = null)
-            : base(storage, contextFactory, new ScoreStore(contextFactory, storage), importHost)
+        public ScoreModelManager(RulesetStore rulesets, Func<BeatmapManager> beatmaps, Storage storage, RealmContextFactory contextFactory)
+            : base(storage, contextFactory)
         {
             this.rulesets = rulesets;
             this.beatmaps = beatmaps;
         }
 
-        protected override ScoreInfo CreateModel(ArchiveReader archive)
+        protected override ScoreInfo? CreateModel(ArchiveReader archive)
         {
-            if (archive == null)
-                return null;
-
             using (var stream = archive.GetStream(archive.Filenames.First(f => f.EndsWith(".osr", StringComparison.OrdinalIgnoreCase))))
             {
                 try
@@ -55,17 +54,28 @@ namespace osu.Game.Scoring
 
         public Score GetScore(ScoreInfo score) => new LegacyDatabasedScore(score, rulesets, beatmaps(), Files.Store);
 
-        public List<ScoreInfo> GetAllUsableScores() => ModelStore.ConsumableItems.Where(s => !s.DeletePending).ToList();
+        protected override void Populate(ScoreInfo model, ArchiveReader? archive, Realm realm, CancellationToken cancellationToken = default)
+        {
+            // Ensure the beatmap is not detached.
+            if (!model.BeatmapInfo.IsManaged)
+                model.BeatmapInfo = realm.Find<BeatmapInfo>(model.BeatmapInfo.ID);
 
-        public IEnumerable<ScoreInfo> QueryScores(Expression<Func<ScoreInfo, bool>> query) => ModelStore.ConsumableItems.AsNoTracking().Where(query);
+            if (!model.Ruleset.IsManaged)
+                model.Ruleset = realm.Find<RulesetInfo>(model.Ruleset.ShortName);
 
-        public ScoreInfo Query(Expression<Func<ScoreInfo, bool>> query) => ModelStore.ConsumableItems.AsNoTracking().FirstOrDefault(query);
+            // These properties are known to be non-null, but these final checks ensure a null hasn't come from somewhere (or the refetch has failed).
+            // Under no circumstance do we want these to be written to realm as null.
+            if (model.BeatmapInfo == null) throw new ArgumentNullException(nameof(model.BeatmapInfo));
+            if (model.Ruleset == null) throw new ArgumentNullException(nameof(model.Ruleset));
 
-        protected override Task Populate(ScoreInfo model, ArchiveReader archive, CancellationToken cancellationToken = default)
-            => Task.CompletedTask;
+            if (string.IsNullOrEmpty(model.StatisticsJson))
+                model.StatisticsJson = JsonConvert.SerializeObject(model.Statistics);
+        }
 
-        protected override bool CheckLocalAvailability(ScoreInfo model, IQueryable<ScoreInfo> items)
-            => base.CheckLocalAvailability(model, items)
-               || (model.OnlineID > 0 && items.Any(i => i.OnlineID == model.OnlineID));
+        public override bool IsAvailableLocally(ScoreInfo model)
+        {
+            using (var context = ContextFactory.CreateContext())
+                return context.All<ScoreInfo>().Any(b => b.OnlineID == model.OnlineID);
+        }
     }
 }
diff --git a/osu.Game/Scoring/ScoreStore.cs b/osu.Game/Scoring/ScoreStore.cs
deleted file mode 100644
index fd1f5ae3ec..0000000000
--- a/osu.Game/Scoring/ScoreStore.cs
+++ /dev/null
@@ -1,25 +0,0 @@
-// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using System.Linq;
-using Microsoft.EntityFrameworkCore;
-using osu.Framework.Platform;
-using osu.Game.Database;
-
-namespace osu.Game.Scoring
-{
-    public class ScoreStore : MutableDatabaseBackedStoreWithFileIncludes<ScoreInfo, ScoreFileInfo>
-    {
-        public ScoreStore(IDatabaseContextFactory factory, Storage storage)
-            : base(factory, storage)
-        {
-        }
-
-        protected override IQueryable<ScoreInfo> AddIncludesForConsumption(IQueryable<ScoreInfo> query)
-            => base.AddIncludesForConsumption(query)
-                   .Include(s => s.BeatmapInfo)
-                   .Include(s => s.BeatmapInfo).ThenInclude(b => b.Metadata)
-                   .Include(s => s.BeatmapInfo).ThenInclude(b => b.BeatmapSet).ThenInclude(s => s.Metadata)
-                   .Include(s => s.Ruleset);
-    }
-}
diff --git a/osu.Game/Screens/Edit/Components/Menus/DifficultyMenuItem.cs b/osu.Game/Screens/Edit/Components/Menus/DifficultyMenuItem.cs
index 75dc479c25..f17fe4c3ce 100644
--- a/osu.Game/Screens/Edit/Components/Menus/DifficultyMenuItem.cs
+++ b/osu.Game/Screens/Edit/Components/Menus/DifficultyMenuItem.cs
@@ -13,7 +13,7 @@ namespace osu.Game.Screens.Edit.Components.Menus
         public BeatmapInfo BeatmapInfo { get; }
 
         public DifficultyMenuItem(BeatmapInfo beatmapInfo, bool selected, Action<BeatmapInfo> difficultyChangeFunc)
-            : base(beatmapInfo.DifficultyName ?? "(unnamed)", null)
+            : base(string.IsNullOrEmpty(beatmapInfo.DifficultyName) ? "(unnamed)" : beatmapInfo.DifficultyName, null)
         {
             BeatmapInfo = beatmapInfo;
             State.Value = selected;
diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs
index 39de13899d..9d5d8013b7 100644
--- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs
@@ -15,7 +15,6 @@ using osu.Framework.Input;
 using osu.Framework.Input.Bindings;
 using osu.Framework.Input.Events;
 using osu.Framework.Utils;
-using osu.Game.Graphics;
 using osu.Game.Graphics.UserInterface;
 using osu.Game.Input.Bindings;
 using osu.Game.Rulesets.Edit;
@@ -57,7 +56,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
         }
 
         [BackgroundDependencyLoader]
-        private void load(OsuColour colours)
+        private void load()
         {
             InternalChild = SelectionBox = CreateSelectionBox();
 
diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs
index 2cbfe88519..7d52645aa1 100644
--- a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs
@@ -15,7 +15,6 @@ using osu.Framework.Graphics.Cursor;
 using osu.Framework.Graphics.UserInterface;
 using osu.Framework.Input.Events;
 using osu.Game.Beatmaps.ControlPoints;
-using osu.Game.Graphics;
 using osu.Game.Graphics.UserInterfaceV2;
 using osu.Game.Rulesets.Objects;
 using osu.Game.Screens.Edit.Timing;
@@ -39,7 +38,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
         }
 
         [BackgroundDependencyLoader]
-        private void load(OsuColour colours)
+        private void load()
         {
             volume.BindValueChanged(volume => updateText());
             bank.BindValueChanged(bank => updateText(), true);
diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimingPointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimingPointPiece.cs
index fa51281c55..2df4ef001c 100644
--- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimingPointPiece.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimingPointPiece.cs
@@ -4,7 +4,6 @@
 using osu.Framework.Allocation;
 using osu.Framework.Bindables;
 using osu.Game.Beatmaps.ControlPoints;
-using osu.Game.Graphics;
 
 namespace osu.Game.Screens.Edit.Compose.Components.Timeline
 {
@@ -19,7 +18,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
         }
 
         [BackgroundDependencyLoader]
-        private void load(OsuColour colours)
+        private void load()
         {
             beatLength.BindValueChanged(beatLength =>
             {
diff --git a/osu.Game/Screens/Edit/Compose/ComposeScreen.cs b/osu.Game/Screens/Edit/Compose/ComposeScreen.cs
index 9386538a78..2bdf59b21c 100644
--- a/osu.Game/Screens/Edit/Compose/ComposeScreen.cs
+++ b/osu.Game/Screens/Edit/Compose/ComposeScreen.cs
@@ -40,7 +40,7 @@ namespace osu.Game.Screens.Edit.Compose
         {
             var dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
 
-            ruleset = parent.Get<IBindable<WorkingBeatmap>>().Value.BeatmapInfo.Ruleset?.CreateInstance();
+            ruleset = parent.Get<IBindable<WorkingBeatmap>>().Value.BeatmapInfo.Ruleset.CreateInstance();
             composer = ruleset?.CreateHitObjectComposer();
 
             // make the composer available to the timeline and other components in this screen.
diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs
index 48489c60ab..8c4b458534 100644
--- a/osu.Game/Screens/Edit/Editor.cs
+++ b/osu.Game/Screens/Edit/Editor.cs
@@ -3,6 +3,7 @@
 
 using System;
 using System.Collections.Generic;
+using System.Diagnostics;
 using System.Linq;
 using JetBrains.Annotations;
 using osu.Framework;
@@ -355,9 +356,6 @@ namespace osu.Game.Screens.Edit
             // no longer new after first user-triggered save.
             isNewBeatmap = false;
 
-            // apply any set-level metadata changes.
-            beatmapManager.Update(editorBeatmap.BeatmapInfo.BeatmapSet);
-
             // save the loaded beatmap's data stream.
             beatmapManager.Save(editorBeatmap.BeatmapInfo, editorBeatmap.PlayableBeatmap, editorBeatmap.BeatmapSkin);
 
@@ -601,7 +599,8 @@ namespace osu.Game.Screens.Edit
             if (isNewBeatmap)
             {
                 // confirming exit without save means we should delete the new beatmap completely.
-                beatmapManager.Delete(playableBeatmap.BeatmapInfo.BeatmapSet);
+                if (playableBeatmap.BeatmapInfo.BeatmapSet != null)
+                    beatmapManager.Delete(playableBeatmap.BeatmapInfo.BeatmapSet);
 
                 // eagerly clear contents before restoring default beatmap to prevent value change callbacks from firing.
                 ClearInternal();
@@ -775,7 +774,9 @@ namespace osu.Game.Screens.Edit
 
             fileMenuItems.Add(new EditorMenuItemSpacer());
 
-            var beatmapSet = beatmapManager.QueryBeatmapSet(bs => bs.ID == Beatmap.Value.BeatmapSetInfo.ID) ?? playableBeatmap.BeatmapInfo.BeatmapSet;
+            var beatmapSet = playableBeatmap.BeatmapInfo.BeatmapSet;
+
+            Debug.Assert(beatmapSet != null);
 
             var difficultyItems = new List<MenuItem>();
 
diff --git a/osu.Game/Screens/Edit/EditorBeatmap.cs b/osu.Game/Screens/Edit/EditorBeatmap.cs
index 98fad09192..c9449f3259 100644
--- a/osu.Game/Screens/Edit/EditorBeatmap.cs
+++ b/osu.Game/Screens/Edit/EditorBeatmap.cs
@@ -102,7 +102,7 @@ namespace osu.Game.Screens.Edit
             if (beatmapSkin is Skin skin)
                 BeatmapSkin = new EditorBeatmapSkin(skin);
 
-            beatmapProcessor = playableBeatmap.BeatmapInfo.Ruleset?.CreateInstance().CreateBeatmapProcessor(PlayableBeatmap);
+            beatmapProcessor = playableBeatmap.BeatmapInfo.Ruleset.CreateInstance().CreateBeatmapProcessor(PlayableBeatmap);
 
             foreach (var obj in HitObjects)
                 trackStartTime(obj);
diff --git a/osu.Game/Screens/Edit/EditorRoundedScreenSettingsSection.cs b/osu.Game/Screens/Edit/EditorRoundedScreenSettingsSection.cs
index e17114ebcb..25d7dfbb4a 100644
--- a/osu.Game/Screens/Edit/EditorRoundedScreenSettingsSection.cs
+++ b/osu.Game/Screens/Edit/EditorRoundedScreenSettingsSection.cs
@@ -6,7 +6,6 @@ using osu.Framework.Graphics;
 using osu.Framework.Graphics.Containers;
 using osu.Framework.Graphics.Sprites;
 using osu.Game.Graphics.Sprites;
-using osu.Game.Overlays;
 using osuTK;
 
 namespace osu.Game.Screens.Edit
@@ -20,7 +19,7 @@ namespace osu.Game.Screens.Edit
         protected FillFlowContainer Flow { get; private set; }
 
         [BackgroundDependencyLoader]
-        private void load(OverlayColourProvider colours)
+        private void load()
         {
             RelativeSizeAxes = Axes.X;
             AutoSizeAxes = Axes.Y;
diff --git a/osu.Game/Screens/Edit/Setup/LabelledRomanisedTextBox.cs b/osu.Game/Screens/Edit/Setup/LabelledRomanisedTextBox.cs
index ee9d86029e..c39b4d6f41 100644
--- a/osu.Game/Screens/Edit/Setup/LabelledRomanisedTextBox.cs
+++ b/osu.Game/Screens/Edit/Setup/LabelledRomanisedTextBox.cs
@@ -13,6 +13,8 @@ namespace osu.Game.Screens.Edit.Setup
 
         private class RomanisedTextBox : OsuTextBox
         {
+            protected override bool AllowIme => false;
+
             protected override bool CanAddCharacter(char character)
                 => MetadataUtils.IsRomanised(character);
         }
diff --git a/osu.Game/Screens/Edit/Setup/MetadataSection.cs b/osu.Game/Screens/Edit/Setup/MetadataSection.cs
index 0d2b093a2e..f0ca3e1bbc 100644
--- a/osu.Game/Screens/Edit/Setup/MetadataSection.cs
+++ b/osu.Game/Screens/Edit/Setup/MetadataSection.cs
@@ -110,7 +110,7 @@ namespace osu.Game.Screens.Edit.Setup
             Beatmap.Metadata.TitleUnicode = TitleTextBox.Current.Value;
             Beatmap.Metadata.Title = RomanisedTitleTextBox.Current.Value;
 
-            Beatmap.Metadata.AuthorString = creatorTextBox.Current.Value;
+            Beatmap.Metadata.Author.Username = creatorTextBox.Current.Value;
             Beatmap.BeatmapInfo.DifficultyName = difficultyTextBox.Current.Value;
             Beatmap.Metadata.Source = sourceTextBox.Current.Value;
             Beatmap.Metadata.Tags = tagsTextBox.Current.Value;
diff --git a/osu.Game/Screens/Edit/Setup/SetupScreen.cs b/osu.Game/Screens/Edit/Setup/SetupScreen.cs
index 8d726f7752..231d977aab 100644
--- a/osu.Game/Screens/Edit/Setup/SetupScreen.cs
+++ b/osu.Game/Screens/Edit/Setup/SetupScreen.cs
@@ -33,7 +33,7 @@ namespace osu.Game.Screens.Edit.Setup
                 new DesignSection(),
             };
 
-            var rulesetSpecificSection = beatmap.BeatmapInfo.Ruleset?.CreateInstance().CreateEditorSetupSection();
+            var rulesetSpecificSection = beatmap.BeatmapInfo.Ruleset.CreateInstance().CreateEditorSetupSection();
             if (rulesetSpecificSection != null)
                 sectionsEnumerable.Add(rulesetSpecificSection);
 
diff --git a/osu.Game/Screens/Edit/Verify/IssueList.cs b/osu.Game/Screens/Edit/Verify/IssueList.cs
index cadcdebc6e..5fe43199cc 100644
--- a/osu.Game/Screens/Edit/Verify/IssueList.cs
+++ b/osu.Game/Screens/Edit/Verify/IssueList.cs
@@ -40,7 +40,7 @@ namespace osu.Game.Screens.Edit.Verify
         private void load(OverlayColourProvider colours)
         {
             generalVerifier = new BeatmapVerifier();
-            rulesetVerifier = beatmap.BeatmapInfo.Ruleset?.CreateInstance().CreateBeatmapVerifier();
+            rulesetVerifier = beatmap.BeatmapInfo.Ruleset.CreateInstance().CreateBeatmapVerifier();
 
             context = new BeatmapVerifierContext(beatmap, workingBeatmap.Value, verify.InterpretedDifficulty.Value);
             verify.InterpretedDifficulty.BindValueChanged(difficulty => context.InterpretedDifficulty = difficulty.NewValue);
diff --git a/osu.Game/Screens/Edit/Verify/VerifyScreen.cs b/osu.Game/Screens/Edit/Verify/VerifyScreen.cs
index 6d7a4a72e2..08643eb8c1 100644
--- a/osu.Game/Screens/Edit/Verify/VerifyScreen.cs
+++ b/osu.Game/Screens/Edit/Verify/VerifyScreen.cs
@@ -29,7 +29,7 @@ namespace osu.Game.Screens.Edit.Verify
         [BackgroundDependencyLoader]
         private void load()
         {
-            InterpretedDifficulty.Default = EditorBeatmap.BeatmapInfo.DifficultyRating;
+            InterpretedDifficulty.Default = BeatmapDifficultyCache.GetDifficultyRating(EditorBeatmap.BeatmapInfo.StarRating);
             InterpretedDifficulty.SetDefault();
 
             IssueList = new IssueList();
diff --git a/osu.Game/Screens/Edit/Verify/VisibilitySection.cs b/osu.Game/Screens/Edit/Verify/VisibilitySection.cs
index d049436376..0bdc8c0efd 100644
--- a/osu.Game/Screens/Edit/Verify/VisibilitySection.cs
+++ b/osu.Game/Screens/Edit/Verify/VisibilitySection.cs
@@ -4,7 +4,6 @@
 using osu.Framework.Allocation;
 using osu.Framework.Bindables;
 using osu.Framework.Graphics;
-using osu.Game.Overlays;
 using osu.Game.Overlays.Settings;
 using osu.Game.Rulesets.Edit.Checks.Components;
 
@@ -24,7 +23,7 @@ namespace osu.Game.Screens.Edit.Verify
         protected override string HeaderText => "Visibility";
 
         [BackgroundDependencyLoader]
-        private void load(OverlayColourProvider colours, VerifyScreen verify)
+        private void load(VerifyScreen verify)
         {
             hiddenIssueTypes = verify.HiddenIssueTypes.GetBoundCopy();
 
diff --git a/osu.Game/Screens/Import/FileImportScreen.cs b/osu.Game/Screens/Import/FileImportScreen.cs
index c32e230e11..09870e0bab 100644
--- a/osu.Game/Screens/Import/FileImportScreen.cs
+++ b/osu.Game/Screens/Import/FileImportScreen.cs
@@ -9,7 +9,6 @@ using osu.Framework.Bindables;
 using osu.Framework.Graphics;
 using osu.Framework.Graphics.Containers;
 using osu.Framework.Graphics.Shapes;
-using osu.Framework.Platform;
 using osu.Framework.Screens;
 using osu.Game.Graphics;
 using osu.Game.Graphics.Containers;
@@ -40,7 +39,7 @@ namespace osu.Game.Screens.Import
         private OsuColour colours { get; set; }
 
         [BackgroundDependencyLoader(true)]
-        private void load(Storage storage)
+        private void load()
         {
             InternalChild = contentContainer = new Container
             {
diff --git a/osu.Game/Screens/Menu/ButtonSystem.cs b/osu.Game/Screens/Menu/ButtonSystem.cs
index feb6f6c92a..32731407fd 100644
--- a/osu.Game/Screens/Menu/ButtonSystem.cs
+++ b/osu.Game/Screens/Menu/ButtonSystem.cs
@@ -15,7 +15,6 @@ using osu.Framework.Graphics.Containers;
 using osu.Framework.Graphics.Sprites;
 using osu.Framework.Input.Bindings;
 using osu.Framework.Input.Events;
-using osu.Framework.Localisation;
 using osu.Framework.Logging;
 using osu.Framework.Platform;
 using osu.Framework.Threading;
@@ -123,7 +122,7 @@ namespace osu.Game.Screens.Menu
         private LoginOverlay loginOverlay { get; set; }
 
         [BackgroundDependencyLoader(true)]
-        private void load(AudioManager audio, IdleTracker idleTracker, GameHost host, LocalisationManager strings)
+        private void load(AudioManager audio, IdleTracker idleTracker, GameHost host)
         {
             buttonsPlay.Add(new Button(ButtonSystemStrings.Solo, @"button-solo-select", FontAwesome.Solid.User, new Color4(102, 68, 204, 255), () => OnSolo?.Invoke(), WEDGE_WIDTH, Key.P));
             buttonsPlay.Add(new Button(ButtonSystemStrings.Multi, @"button-generic-select", FontAwesome.Solid.Users, new Color4(94, 63, 186, 255), onMultiplayer, 0, Key.M));
diff --git a/osu.Game/Screens/Menu/IntroScreen.cs b/osu.Game/Screens/Menu/IntroScreen.cs
index 948e3a7d88..d98cb8056f 100644
--- a/osu.Game/Screens/Menu/IntroScreen.cs
+++ b/osu.Game/Screens/Menu/IntroScreen.cs
@@ -15,10 +15,10 @@ using osu.Framework.Screens;
 using osu.Framework.Utils;
 using osu.Game.Beatmaps;
 using osu.Game.Configuration;
+using osu.Game.Database;
 using osu.Game.IO.Archives;
 using osu.Game.Overlays;
 using osu.Game.Screens.Backgrounds;
-using osu.Game.Skinning;
 using osuTK;
 using osuTK.Graphics;
 
@@ -80,27 +80,36 @@ namespace osu.Game.Screens.Menu
             this.createNextScreen = createNextScreen;
         }
 
+        [Resolved]
+        private BeatmapManager beatmaps { get; set; }
+
         [BackgroundDependencyLoader]
-        private void load(OsuConfigManager config, SkinManager skinManager, BeatmapManager beatmaps, Framework.Game game)
+        private void load(OsuConfigManager config, Framework.Game game, RealmContextFactory realmContextFactory)
         {
-            // prevent user from changing beatmap while the intro is still runnning.
+            // prevent user from changing beatmap while the intro is still running.
             beatmap = Beatmap.BeginLease(false);
 
             MenuVoice = config.GetBindable<bool>(OsuSetting.MenuVoice);
             MenuMusic = config.GetBindable<bool>(OsuSetting.MenuMusic);
             seeya = audio.Samples.Get(SeeyaSampleName);
 
-            BeatmapSetInfo setInfo = null;
+            ILive<BeatmapSetInfo> setInfo = null;
 
             // if the user has requested not to play theme music, we should attempt to find a random beatmap from their collection.
             if (!MenuMusic.Value)
             {
-                var sets = beatmaps.GetAllUsableBeatmapSets(IncludedDetails.Minimal);
+                var sets = beatmaps.GetAllUsableBeatmapSets();
 
                 if (sets.Count > 0)
                 {
                     setInfo = beatmaps.QueryBeatmapSet(s => s.ID == sets[RNG.Next(0, sets.Count - 1)].ID);
-                    initialBeatmap = beatmaps.GetWorkingBeatmap(setInfo.Beatmaps[0]);
+                    setInfo?.PerformRead(s =>
+                    {
+                        if (s.Beatmaps.Count == 0)
+                            return;
+
+                        initialBeatmap = beatmaps.GetWorkingBeatmap(s.Beatmaps[0]);
+                    });
                 }
             }
 
@@ -113,11 +122,7 @@ namespace osu.Game.Screens.Menu
                     // this could happen if a user has nuked their files store. for now, reimport to repair this.
                     var import = beatmaps.Import(new ZipArchiveReader(game.Resources.GetStream($"Tracks/{BeatmapFile}"), BeatmapFile)).GetResultSafely();
 
-                    import.PerformWrite(b =>
-                    {
-                        b.Protected = true;
-                        beatmaps.Update(b);
-                    });
+                    import?.PerformWrite(b => b.Protected = true);
 
                     loadThemedIntro();
                 }
@@ -125,12 +130,18 @@ namespace osu.Game.Screens.Menu
 
             bool loadThemedIntro()
             {
-                setInfo = beatmaps.QueryBeatmapSets(b => b.Hash == BeatmapHash, IncludedDetails.AllButRuleset).FirstOrDefault();
+                setInfo = beatmaps.QueryBeatmapSet(b => b.Hash == BeatmapHash);
 
                 if (setInfo == null)
                     return false;
 
-                initialBeatmap = beatmaps.GetWorkingBeatmap(setInfo.Beatmaps[0]);
+                setInfo.PerformRead(s =>
+                {
+                    if (s.Beatmaps.Count == 0)
+                        return;
+
+                    initialBeatmap = beatmaps.GetWorkingBeatmap(s.Beatmaps.First());
+                });
 
                 return UsingThemedIntro = initialBeatmap != null;
             }
@@ -199,8 +210,11 @@ namespace osu.Game.Screens.Menu
 
             if (!resuming)
             {
-                beatmap.Value = initialBeatmap;
-                Track = initialBeatmap.Track;
+                // generally this can never be null
+                // an exception is running ruleset tests, where the osu! ruleset may not be present (causing importing the intro to fail).
+                if (initialBeatmap != null)
+                    beatmap.Value = initialBeatmap;
+                Track = beatmap.Value.Track;
 
                 // ensure the track starts at maximum volume
                 musicController.CurrentTrack.FinishTransforms();
diff --git a/osu.Game/Screens/Menu/IntroTriangles.cs b/osu.Game/Screens/Menu/IntroTriangles.cs
index d171e481b1..10f940e9de 100644
--- a/osu.Game/Screens/Menu/IntroTriangles.cs
+++ b/osu.Game/Screens/Menu/IntroTriangles.cs
@@ -133,7 +133,7 @@ namespace osu.Game.Screens.Menu
             private OsuGameBase game { get; set; }
 
             [BackgroundDependencyLoader]
-            private void load(TextureStore textures)
+            private void load()
             {
                 InternalChildren = new Drawable[]
                 {
diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs
index 3da740b85d..8b1bab52b3 100644
--- a/osu.Game/Screens/Menu/MainMenu.cs
+++ b/osu.Game/Screens/Menu/MainMenu.cs
@@ -71,7 +71,7 @@ namespace osu.Game.Screens.Menu
         private SongTicker songTicker;
 
         [BackgroundDependencyLoader(true)]
-        private void load(BeatmapListingOverlay beatmapListing, SettingsOverlay settings, RankingsOverlay rankings, OsuConfigManager config, SessionStatics statics)
+        private void load(BeatmapListingOverlay beatmapListing, SettingsOverlay settings, OsuConfigManager config, SessionStatics statics)
         {
             holdDelay = config.GetBindable<float>(OsuSetting.UIHoldActivationDelay);
             loginDisplayed = statics.GetBindable<bool>(Static.LoginOverlayDisplayed);
diff --git a/osu.Game/Screens/OnlinePlay/Match/Components/MatchLeaderboardScore.cs b/osu.Game/Screens/OnlinePlay/Match/Components/MatchLeaderboardScore.cs
index e8f5b1e826..799c44cc28 100644
--- a/osu.Game/Screens/OnlinePlay/Match/Components/MatchLeaderboardScore.cs
+++ b/osu.Game/Screens/OnlinePlay/Match/Components/MatchLeaderboardScore.cs
@@ -14,8 +14,8 @@ namespace osu.Game.Screens.OnlinePlay.Match.Components
     {
         private readonly APIUserScoreAggregate score;
 
-        public MatchLeaderboardScore(APIUserScoreAggregate score, int? rank, bool allowHighlight = true)
-            : base(score.CreateScoreInfo(), rank, allowHighlight)
+        public MatchLeaderboardScore(APIUserScoreAggregate score, int? rank, bool isOnlineScope = true)
+            : base(score.CreateScoreInfo(), rank, isOnlineScope)
         {
             this.score = score;
         }
diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs
index c31239616c..2d5225639f 100644
--- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs
+++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs
@@ -21,6 +21,7 @@ using osu.Game.Overlays.Mods;
 using osu.Game.Rulesets;
 using osu.Game.Rulesets.Mods;
 using osu.Game.Screens.OnlinePlay.Match.Components;
+using osu.Game.Screens.OnlinePlay.Multiplayer;
 
 namespace osu.Game.Screens.OnlinePlay.Match
 {
@@ -101,6 +102,7 @@ namespace osu.Game.Screens.OnlinePlay.Match
             InternalChildren = new Drawable[]
             {
                 beatmapAvailabilityTracker,
+                new MultiplayerRoomSounds(),
                 new GridContainer
                 {
                     RelativeSizeAxes = Axes.Both,
diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomSounds.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomSounds.cs
new file mode 100644
index 0000000000..d467a32acb
--- /dev/null
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomSounds.cs
@@ -0,0 +1,65 @@
+// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Audio;
+using osu.Framework.Audio.Sample;
+using osu.Framework.Bindables;
+using osu.Game.Online.API.Requests.Responses;
+using osu.Game.Online.Multiplayer;
+
+namespace osu.Game.Screens.OnlinePlay.Multiplayer
+{
+    public class MultiplayerRoomSounds : MultiplayerRoomComposite
+    {
+        private Sample hostChangedSample;
+        private Sample userJoinedSample;
+        private Sample userLeftSample;
+        private Sample userKickedSample;
+
+        [BackgroundDependencyLoader]
+        private void load(AudioManager audio)
+        {
+            hostChangedSample = audio.Samples.Get(@"Multiplayer/host-changed");
+            userJoinedSample = audio.Samples.Get(@"Multiplayer/player-joined");
+            userLeftSample = audio.Samples.Get(@"Multiplayer/player-left");
+            userKickedSample = audio.Samples.Get(@"Multiplayer/player-kicked");
+        }
+
+        protected override void LoadComplete()
+        {
+            base.LoadComplete();
+
+            Host.BindValueChanged(hostChanged);
+        }
+
+        protected override void UserJoined(MultiplayerRoomUser user)
+        {
+            base.UserJoined(user);
+
+            userJoinedSample?.Play();
+        }
+
+        protected override void UserLeft(MultiplayerRoomUser user)
+        {
+            base.UserLeft(user);
+
+            userLeftSample?.Play();
+        }
+
+        protected override void UserKicked(MultiplayerRoomUser user)
+        {
+            base.UserKicked(user);
+
+            userKickedSample?.Play();
+        }
+
+        private void hostChanged(ValueChangedEvent<APIUser> value)
+        {
+            // only play sound when the host changes from an already-existing host.
+            if (value.OldValue == null) return;
+
+            hostChangedSample?.Play();
+        }
+    }
+}
diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantsList.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantsList.cs
index d36c556fac..fe40a4bfe6 100644
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantsList.cs
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantsList.cs
@@ -4,12 +4,10 @@
 using System.Linq;
 using osu.Framework.Allocation;
 using osu.Framework.Audio;
-using osu.Framework.Audio.Sample;
 using osu.Framework.Graphics;
 using osu.Framework.Graphics.Containers;
 using osu.Game.Graphics.Containers;
 using osu.Game.Graphics.Cursor;
-using osu.Game.Online.Multiplayer;
 using osuTK;
 
 namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
@@ -18,10 +16,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
     {
         private FillFlowContainer<ParticipantPanel> panels;
 
-        private Sample userJoinSample;
-        private Sample userLeftSample;
-        private Sample userKickedSample;
-
         [BackgroundDependencyLoader]
         private void load(AudioManager audio)
         {
@@ -41,31 +35,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
                     }
                 }
             };
-
-            userJoinSample = audio.Samples.Get(@"Multiplayer/player-joined");
-            userLeftSample = audio.Samples.Get(@"Multiplayer/player-left");
-            userKickedSample = audio.Samples.Get(@"Multiplayer/player-kicked");
-        }
-
-        protected override void UserJoined(MultiplayerRoomUser user)
-        {
-            base.UserJoined(user);
-
-            userJoinSample?.Play();
-        }
-
-        protected override void UserLeft(MultiplayerRoomUser user)
-        {
-            base.UserLeft(user);
-
-            userLeftSample?.Play();
-        }
-
-        protected override void UserKicked(MultiplayerRoomUser user)
-        {
-            base.UserKicked(user);
-
-            userKickedSample?.Play();
         }
 
         protected override void OnRoomUpdated()
diff --git a/osu.Game/Screens/OsuScreen.cs b/osu.Game/Screens/OsuScreen.cs
index ccc891d3bf..ed4901e1fa 100644
--- a/osu.Game/Screens/OsuScreen.cs
+++ b/osu.Game/Screens/OsuScreen.cs
@@ -145,7 +145,7 @@ namespace osu.Game.Screens
         }
 
         [BackgroundDependencyLoader(true)]
-        private void load(OsuGame osu, AudioManager audio)
+        private void load(AudioManager audio)
         {
             sampleExit = audio.Samples.Get(@"UI/screen-back");
         }
diff --git a/osu.Game/Screens/Play/EpilepsyWarning.cs b/osu.Game/Screens/Play/EpilepsyWarning.cs
index 89e25d849f..ccb2870d78 100644
--- a/osu.Game/Screens/Play/EpilepsyWarning.cs
+++ b/osu.Game/Screens/Play/EpilepsyWarning.cs
@@ -2,11 +2,9 @@
 // See the LICENCE file in the repository root for full licence text.
 
 using osu.Framework.Allocation;
-using osu.Framework.Bindables;
 using osu.Framework.Graphics;
 using osu.Framework.Graphics.Containers;
 using osu.Framework.Graphics.Sprites;
-using osu.Game.Beatmaps;
 using osu.Game.Graphics;
 using osu.Game.Graphics.Containers;
 using osu.Game.Screens.Backgrounds;
@@ -39,7 +37,7 @@ namespace osu.Game.Screens.Play
         }
 
         [BackgroundDependencyLoader]
-        private void load(OsuColour colours, IBindable<WorkingBeatmap> beatmap)
+        private void load(OsuColour colours)
         {
             Children = new Drawable[]
             {
diff --git a/osu.Game/Screens/Play/GameplayState.cs b/osu.Game/Screens/Play/GameplayState.cs
index 44f72022f7..83881f739d 100644
--- a/osu.Game/Screens/Play/GameplayState.cs
+++ b/osu.Game/Screens/Play/GameplayState.cs
@@ -50,7 +50,13 @@ namespace osu.Game.Screens.Play
         {
             Beatmap = beatmap;
             Ruleset = ruleset;
-            Score = score ?? new Score();
+            Score = score ?? new Score
+            {
+                ScoreInfo =
+                {
+                    Ruleset = ruleset.RulesetInfo
+                }
+            };
             Mods = mods ?? ArraySegment<Mod>.Empty;
         }
 
diff --git a/osu.Game/Screens/Play/HUD/HoldForMenuButton.cs b/osu.Game/Screens/Play/HUD/HoldForMenuButton.cs
index 8e0a38aa1f..5a7ef786d3 100644
--- a/osu.Game/Screens/Play/HUD/HoldForMenuButton.cs
+++ b/osu.Game/Screens/Play/HUD/HoldForMenuButton.cs
@@ -116,7 +116,7 @@ namespace osu.Game.Screens.Play.HUD
             public Action HoverLost;
 
             [BackgroundDependencyLoader]
-            private void load(OsuColour colours, Framework.Game game)
+            private void load(OsuColour colours)
             {
                 Size = new Vector2(60);
 
diff --git a/osu.Game/Screens/Play/HUD/UnstableRateCounter.cs b/osu.Game/Screens/Play/HUD/UnstableRateCounter.cs
index 235f0f01fd..a71b661965 100644
--- a/osu.Game/Screens/Play/HUD/UnstableRateCounter.cs
+++ b/osu.Game/Screens/Play/HUD/UnstableRateCounter.cs
@@ -8,7 +8,6 @@ using osu.Framework.Graphics;
 using osu.Framework.Graphics.Containers;
 using osu.Framework.Graphics.Sprites;
 using osu.Framework.Localisation;
-using osu.Game.Beatmaps;
 using osu.Game.Graphics;
 using osu.Game.Graphics.Sprites;
 using osu.Game.Graphics.UserInterface;
@@ -37,7 +36,7 @@ namespace osu.Game.Screens.Play.HUD
         }
 
         [BackgroundDependencyLoader]
-        private void load(OsuColour colours, BeatmapDifficultyCache difficultyCache)
+        private void load(OsuColour colours)
         {
             Colour = colours.BlueLighter;
             valid.BindValueChanged(e =>
diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs
index 2a6f5e2398..4d3201cd27 100644
--- a/osu.Game/Screens/Play/Player.cs
+++ b/osu.Game/Screens/Play/Player.cs
@@ -226,8 +226,6 @@ namespace osu.Game.Screens.Play
             // ensure the score is in a consistent state with the current player.
             Score.ScoreInfo.BeatmapInfo = Beatmap.Value.BeatmapInfo;
             Score.ScoreInfo.Ruleset = ruleset.RulesetInfo;
-            if (ruleset.RulesetInfo.ID != null)
-                Score.ScoreInfo.RulesetID = ruleset.RulesetInfo.ID.Value;
             Score.ScoreInfo.Mods = gameplayMods;
 
             dependencies.CacheAs(GameplayState = new GameplayState(playableBeatmap, ruleset, gameplayMods, Score));
@@ -488,6 +486,9 @@ namespace osu.Game.Screens.Play
                 var rulesetInfo = Ruleset.Value ?? Beatmap.Value.BeatmapInfo.Ruleset;
                 ruleset = rulesetInfo.CreateInstance();
 
+                if (ruleset == null)
+                    throw new RulesetLoadException("Instantiation failure");
+
                 try
                 {
                     playable = Beatmap.Value.GetPlayableBeatmap(ruleset.RulesetInfo, gameplayMods);
@@ -1037,18 +1038,24 @@ namespace osu.Game.Screens.Play
                 replayReader = new LegacyByteArrayReader(stream.ToArray(), "replay.osr");
             }
 
+            // the import process will re-attach managed beatmap/rulesets to this score. we don't want this for now, so create a temporary copy to import.
+            var importableScore = score.ScoreInfo.DeepClone();
+
             // For the time being, online ID responses are not really useful for anything.
             // In addition, the IDs provided via new (lazer) endpoints are based on a different autoincrement from legacy (stable) scores.
             //
             // Until we better define the server-side logic behind this, let's not store the online ID to avoid potential unique constraint
             // conflicts across various systems (ie. solo and multiplayer).
-            long? onlineScoreId = score.ScoreInfo.OnlineID;
-            score.ScoreInfo.OnlineID = -1;
+            importableScore.OnlineID = -1;
 
-            await scoreManager.Import(score.ScoreInfo, replayReader).ConfigureAwait(false);
+            var imported = await scoreManager.Import(importableScore, replayReader).ConfigureAwait(false);
 
-            // ... And restore the online ID for other processes to handle correctly (e.g. de-duplication for the results screen).
-            score.ScoreInfo.OnlineID = onlineScoreId;
+            imported.PerformRead(s =>
+            {
+                // because of the clone above, it's required that we copy back the post-import hash/ID to use for availability matching.
+                score.ScoreInfo.Hash = s.Hash;
+                score.ScoreInfo.ID = s.ID;
+            });
         }
 
         /// <summary>
diff --git a/osu.Game/Screens/Play/SoloPlayer.cs b/osu.Game/Screens/Play/SoloPlayer.cs
index c8d831ebe6..eced2d142b 100644
--- a/osu.Game/Screens/Play/SoloPlayer.cs
+++ b/osu.Game/Screens/Play/SoloPlayer.cs
@@ -26,7 +26,7 @@ namespace osu.Game.Screens.Play
 
         protected override APIRequest<APIScoreToken> CreateTokenRequest()
         {
-            int beatmapId = Beatmap.Value.BeatmapInfo.OnlineID ?? -1;
+            int beatmapId = Beatmap.Value.BeatmapInfo.OnlineID;
             int rulesetId = Ruleset.Value.OnlineID;
 
             if (beatmapId <= 0)
diff --git a/osu.Game/Screens/Ranking/Contracted/ContractedPanelMiddleContent.cs b/osu.Game/Screens/Ranking/Contracted/ContractedPanelMiddleContent.cs
index 20c603295b..f9aff28bef 100644
--- a/osu.Game/Screens/Ranking/Contracted/ContractedPanelMiddleContent.cs
+++ b/osu.Game/Screens/Ranking/Contracted/ContractedPanelMiddleContent.cs
@@ -107,7 +107,7 @@ namespace osu.Game.Screens.Ranking.Contracted
                                         {
                                             Anchor = Anchor.TopCentre,
                                             Origin = Anchor.TopCentre,
-                                            Text = score.UserString,
+                                            Text = score.RealmUser.Username,
                                             Font = OsuFont.GetFont(size: 16, weight: FontWeight.SemiBold)
                                         },
                                         new FillFlowContainer
diff --git a/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs b/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs
index 635be60549..e50520e0ca 100644
--- a/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs
+++ b/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs
@@ -10,7 +10,6 @@ using osu.Framework.Extensions.Color4Extensions;
 using osu.Framework.Graphics;
 using osu.Framework.Graphics.Colour;
 using osu.Framework.Graphics.Containers;
-using osu.Framework.Platform;
 using osu.Framework.Utils;
 using osu.Game.Audio;
 using osu.Game.Graphics;
@@ -104,7 +103,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy
         }
 
         [BackgroundDependencyLoader]
-        private void load(GameHost host)
+        private void load()
         {
             InternalChildren = new Drawable[]
             {
diff --git a/osu.Game/Screens/Ranking/ScorePanelList.cs b/osu.Game/Screens/Ranking/ScorePanelList.cs
index f8e9d08350..c2ef5529e8 100644
--- a/osu.Game/Screens/Ranking/ScorePanelList.cs
+++ b/osu.Game/Screens/Ranking/ScorePanelList.cs
@@ -158,7 +158,7 @@ namespace osu.Game.Screens.Ranking
 
                             trackingContainer.Show();
 
-                            if (SelectedScore.Value == score)
+                            if (SelectedScore.Value?.Equals(score) == true)
                             {
                                 SelectedScore.TriggerChange();
                             }
@@ -185,10 +185,10 @@ namespace osu.Game.Screens.Ranking
         private void selectedScoreChanged(ValueChangedEvent<ScoreInfo> score)
         {
             // avoid contracting panels unnecessarily when TriggerChange is fired manually.
-            if (score.OldValue != score.NewValue)
+            if (score.OldValue != null && !score.OldValue.Equals(score.NewValue))
             {
                 // Contract the old panel.
-                foreach (var t in flow.Where(t => t.Panel.Score == score.OldValue))
+                foreach (var t in flow.Where(t => t.Panel.Score.Equals(score.OldValue)))
                 {
                     t.Panel.State = PanelState.Contracted;
                     t.Margin = new MarginPadding();
@@ -196,7 +196,7 @@ namespace osu.Game.Screens.Ranking
             }
 
             // Find the panel corresponding to the new score.
-            var expandedTrackingComponent = flow.SingleOrDefault(t => t.Panel.Score == score.NewValue);
+            var expandedTrackingComponent = flow.SingleOrDefault(t => t.Panel.Score.Equals(score.NewValue));
             expandedPanel = expandedTrackingComponent?.Panel;
 
             if (expandedPanel == null)
@@ -269,7 +269,7 @@ namespace osu.Game.Screens.Ranking
         /// </summary>
         /// <param name="score">The <see cref="ScoreInfo"/> to find the corresponding <see cref="ScorePanel"/> for.</param>
         /// <returns>The <see cref="ScorePanel"/>.</returns>
-        public ScorePanel GetPanelForScore(ScoreInfo score) => flow.Single(t => t.Panel.Score == score).Panel;
+        public ScorePanel GetPanelForScore(ScoreInfo score) => flow.Single(t => t.Panel.Score.Equals(score)).Panel;
 
         /// <summary>
         /// Detaches a <see cref="ScorePanel"/> from its <see cref="ScorePanelTrackingContainer"/>, allowing the panel to be moved elsewhere in the hierarchy.
@@ -332,13 +332,13 @@ namespace osu.Game.Screens.Ranking
         {
             public override IEnumerable<Drawable> FlowingChildren => applySorting(AliveInternalChildren);
 
-            public int GetPanelIndex(ScoreInfo score) => applySorting(Children).TakeWhile(s => s.Panel.Score != score).Count();
+            public int GetPanelIndex(ScoreInfo score) => applySorting(Children).TakeWhile(s => !s.Panel.Score.Equals(score)).Count();
 
             [CanBeNull]
-            public ScoreInfo GetPreviousScore(ScoreInfo score) => applySorting(Children).TakeWhile(s => s.Panel.Score != score).LastOrDefault()?.Panel.Score;
+            public ScoreInfo GetPreviousScore(ScoreInfo score) => applySorting(Children).TakeWhile(s => !s.Panel.Score.Equals(score)).LastOrDefault()?.Panel.Score;
 
             [CanBeNull]
-            public ScoreInfo GetNextScore(ScoreInfo score) => applySorting(Children).SkipWhile(s => s.Panel.Score != score).ElementAtOrDefault(1)?.Panel.Score;
+            public ScoreInfo GetNextScore(ScoreInfo score) => applySorting(Children).SkipWhile(s => !s.Panel.Score.Equals(score)).ElementAtOrDefault(1)?.Panel.Score;
 
             private IEnumerable<ScorePanelTrackingContainer> applySorting(IEnumerable<Drawable> drawables) => drawables.OfType<ScorePanelTrackingContainer>()
                                                                                                                        .OrderByDescending(GetLayoutPosition)
diff --git a/osu.Game/Screens/Ranking/SoloResultsScreen.cs b/osu.Game/Screens/Ranking/SoloResultsScreen.cs
index afebc728b4..2ec6c38287 100644
--- a/osu.Game/Screens/Ranking/SoloResultsScreen.cs
+++ b/osu.Game/Screens/Ranking/SoloResultsScreen.cs
@@ -27,7 +27,7 @@ namespace osu.Game.Screens.Ranking
 
         protected override APIRequest FetchScores(Action<IEnumerable<ScoreInfo>> scoresCallback)
         {
-            if (Score.BeatmapInfo.OnlineID == null || Score.BeatmapInfo.Status <= BeatmapOnlineStatus.Pending)
+            if (Score.BeatmapInfo.OnlineID <= 0 || Score.BeatmapInfo.Status <= BeatmapOnlineStatus.Pending)
                 return null;
 
             getScoreRequest = new GetScoresRequest(Score.BeatmapInfo, Score.Ruleset);
diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs
index 26dc3165f8..567a2307dd 100644
--- a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs
+++ b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs
@@ -1,7 +1,6 @@
 // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
 // See the LICENCE file in the repository root for full licence text.
 
-using System;
 using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
@@ -13,7 +12,6 @@ using osu.Framework.Input.Events;
 using osu.Game.Beatmaps;
 using osu.Game.Graphics.UserInterface;
 using osu.Game.Online.Placeholders;
-using osu.Game.Rulesets.Mods;
 using osu.Game.Scoring;
 using osuTK;
 
@@ -76,7 +74,7 @@ namespace osu.Game.Screens.Ranking.Statistics
             if (newScore == null)
                 return;
 
-            if (newScore.HitEvents == null || newScore.HitEvents.Count == 0)
+            if (newScore.HitEvents.Count == 0)
             {
                 content.Add(new FillFlowContainer
                 {
@@ -104,7 +102,7 @@ namespace osu.Game.Screens.Ranking.Statistics
                 // Todo: The placement of this is temporary. Eventually we'll both generate the playable beatmap _and_ run through it in a background task to generate the hit events.
                 Task.Run(() =>
                 {
-                    playableBeatmap = beatmapManager.GetWorkingBeatmap(newScore.BeatmapInfo).GetPlayableBeatmap(newScore.Ruleset, newScore.Mods ?? Array.Empty<Mod>());
+                    playableBeatmap = beatmapManager.GetWorkingBeatmap(newScore.BeatmapInfo).GetPlayableBeatmap(newScore.Ruleset, newScore.Mods);
                 }, loadCancellation.Token).ContinueWith(t => Schedule(() =>
                 {
                     var rows = new FillFlowContainer
@@ -142,7 +140,7 @@ namespace osu.Game.Screens.Ranking.Statistics
 
                     LoadComponentAsync(rows, d =>
                     {
-                        if (Score.Value != newScore)
+                        if (!Score.Value.Equals(newScore))
                             return;
 
                         spinner.Hide();
diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs
index b0d0821ee9..dc67a4eb45 100644
--- a/osu.Game/Screens/Select/BeatmapCarousel.cs
+++ b/osu.Game/Screens/Select/BeatmapCarousel.cs
@@ -18,12 +18,14 @@ using osu.Framework.Threading;
 using osu.Framework.Utils;
 using osu.Game.Beatmaps;
 using osu.Game.Configuration;
+using osu.Game.Database;
 using osu.Game.Graphics.Containers;
 using osu.Game.Graphics.Cursor;
 using osu.Game.Input.Bindings;
 using osu.Game.Screens.Select.Carousel;
 using osuTK;
 using osuTK.Input;
+using Realms;
 
 namespace osu.Game.Screens.Select
 {
@@ -96,10 +98,16 @@ namespace osu.Game.Screens.Select
         private IEnumerable<CarouselBeatmapSet> beatmapSets => root.Children.OfType<CarouselBeatmapSet>();
 
         // todo: only used for testing, maybe remove.
+        private bool loadedTestBeatmaps;
+
         public IEnumerable<BeatmapSetInfo> BeatmapSets
         {
             get => beatmapSets.Select(g => g.BeatmapSet);
-            set => loadBeatmapSets(value);
+            set
+            {
+                loadedTestBeatmaps = true;
+                loadBeatmapSets(value);
+            }
         }
 
         private void loadBeatmapSets(IEnumerable<BeatmapSetInfo> beatmapSets)
@@ -116,8 +124,7 @@ namespace osu.Game.Screens.Select
             itemsCache.Invalidate();
             ScrollToSelected();
 
-            // apply any pending filter operation that may have been delayed (see applyActiveCriteria's scheduling behaviour when BeatmapSetsLoaded is false).
-            FlushPendingFilterOperations();
+            applyActiveCriteria(false);
 
             // Run on late scheduler want to ensure this runs after all pending UpdateBeatmapSet / RemoveBeatmapSet operations are run.
             SchedulerAfterChildren.Add(() =>
@@ -142,6 +149,11 @@ namespace osu.Game.Screens.Select
 
         private CarouselRoot root;
 
+        private IDisposable subscriptionSets;
+        private IDisposable subscriptionDeletedSets;
+        private IDisposable subscriptionBeatmaps;
+        private IDisposable subscriptionHiddenBeatmaps;
+
         private readonly DrawablePool<DrawableCarouselBeatmapSet> setPool = new DrawablePool<DrawableCarouselBeatmapSet>(100);
 
         public BeatmapCarousel()
@@ -161,10 +173,7 @@ namespace osu.Game.Screens.Select
             };
         }
 
-        [Resolved]
-        private BeatmapManager beatmaps { get; set; }
-
-        [BackgroundDependencyLoader(permitNulls: true)]
+        [BackgroundDependencyLoader]
         private void load(OsuConfigManager config)
         {
             config.BindWith(OsuSetting.RandomSelectAlgorithm, RandomAlgorithm);
@@ -172,17 +181,66 @@ namespace osu.Game.Screens.Select
 
             RightClickScrollingEnabled.ValueChanged += enabled => Scroll.RightMouseScrollbar = enabled.NewValue;
             RightClickScrollingEnabled.TriggerChange();
-
-            beatmaps.ItemUpdated += beatmapUpdated;
-            beatmaps.ItemRemoved += beatmapRemoved;
-            beatmaps.BeatmapHidden += beatmapHidden;
-            beatmaps.BeatmapRestored += beatmapRestored;
-
-            if (!beatmapSets.Any())
-                loadBeatmapSets(GetLoadableBeatmaps());
         }
 
-        protected virtual IEnumerable<BeatmapSetInfo> GetLoadableBeatmaps() => beatmaps.GetAllUsableBeatmapSetsEnumerable(IncludedDetails.AllButFiles);
+        [Resolved]
+        private RealmContextFactory realmFactory { get; set; }
+
+        protected override void LoadComplete()
+        {
+            base.LoadComplete();
+
+            subscriptionSets = realmFactory.Context.All<BeatmapSetInfo>().Where(s => !s.DeletePending && !s.Protected).QueryAsyncWithNotifications(beatmapSetsChanged);
+            subscriptionBeatmaps = realmFactory.Context.All<BeatmapInfo>().Where(b => !b.Hidden).QueryAsyncWithNotifications(beatmapsChanged);
+
+            // Can't use main subscriptions because we can't lookup deleted indices.
+            // https://github.com/realm/realm-dotnet/discussions/2634#discussioncomment-1605595.
+            subscriptionDeletedSets = realmFactory.Context.All<BeatmapSetInfo>().Where(s => s.DeletePending && !s.Protected).QueryAsyncWithNotifications(deletedBeatmapSetsChanged);
+            subscriptionHiddenBeatmaps = realmFactory.Context.All<BeatmapInfo>().Where(b => b.Hidden).QueryAsyncWithNotifications(beatmapsChanged);
+        }
+
+        private void deletedBeatmapSetsChanged(IRealmCollection<BeatmapSetInfo> sender, ChangeSet changes, Exception error)
+        {
+            // If loading test beatmaps, avoid overwriting with realm subscription callbacks.
+            if (loadedTestBeatmaps)
+                return;
+
+            if (changes == null)
+                return;
+
+            foreach (int i in changes.InsertedIndices)
+                RemoveBeatmapSet(sender[i]);
+        }
+
+        private void beatmapSetsChanged(IRealmCollection<BeatmapSetInfo> sender, ChangeSet changes, Exception error)
+        {
+            // If loading test beatmaps, avoid overwriting with realm subscription callbacks.
+            if (loadedTestBeatmaps)
+                return;
+
+            if (changes == null)
+            {
+                // initial load
+                loadBeatmapSets(sender);
+                return;
+            }
+
+            foreach (int i in changes.NewModifiedIndices)
+                UpdateBeatmapSet(sender[i]);
+
+            foreach (int i in changes.InsertedIndices)
+                UpdateBeatmapSet(sender[i]);
+        }
+
+        private void beatmapsChanged(IRealmCollection<BeatmapInfo> sender, ChangeSet changes, Exception error)
+        {
+            // we only care about actual changes in hidden status.
+            if (changes == null)
+                return;
+
+            foreach (int i in changes.InsertedIndices)
+                UpdateBeatmapSet(sender[i].BeatmapSet);
+        }
 
         public void RemoveBeatmapSet(BeatmapSetInfo beatmapSet) => Schedule(() =>
         {
@@ -197,7 +255,8 @@ namespace osu.Game.Screens.Select
 
         public void UpdateBeatmapSet(BeatmapSetInfo beatmapSet) => Schedule(() =>
         {
-            int? previouslySelectedID = null;
+            Guid? previouslySelectedID = null;
+
             CarouselBeatmapSet existingSet = beatmapSets.FirstOrDefault(b => b.BeatmapSet.Equals(beatmapSet));
 
             // If the selected beatmap is about to be removed, store its ID so it can be re-selected if required
@@ -616,19 +675,12 @@ namespace osu.Game.Screens.Select
             return (firstIndex, lastIndex);
         }
 
-        private void beatmapRemoved(BeatmapSetInfo item) => RemoveBeatmapSet(item);
-        private void beatmapUpdated(BeatmapSetInfo item) => UpdateBeatmapSet(item);
-        private void beatmapRestored(BeatmapInfo b) => UpdateBeatmapSet(beatmaps.QueryBeatmapSet(s => s.ID == b.BeatmapSetInfoID));
-        private void beatmapHidden(BeatmapInfo b) => UpdateBeatmapSet(beatmaps.QueryBeatmapSet(s => s.ID == b.BeatmapSetInfoID));
-
         private CarouselBeatmapSet createCarouselSet(BeatmapSetInfo beatmapSet)
         {
             if (beatmapSet.Beatmaps.All(b => b.Hidden))
                 return null;
 
-            // todo: remove the need for this.
-            foreach (var b in beatmapSet.Beatmaps)
-                b.Metadata ??= beatmapSet.Metadata;
+            beatmapSet = beatmapSet.Detach();
 
             var set = new CarouselBeatmapSet(beatmapSet)
             {
@@ -883,13 +935,10 @@ namespace osu.Game.Screens.Select
         {
             base.Dispose(isDisposing);
 
-            if (beatmaps != null)
-            {
-                beatmaps.ItemUpdated -= beatmapUpdated;
-                beatmaps.ItemRemoved -= beatmapRemoved;
-                beatmaps.BeatmapHidden -= beatmapHidden;
-                beatmaps.BeatmapRestored -= beatmapRestored;
-            }
+            subscriptionSets?.Dispose();
+            subscriptionDeletedSets?.Dispose();
+            subscriptionBeatmaps?.Dispose();
+            subscriptionHiddenBeatmaps?.Dispose();
         }
     }
 }
diff --git a/osu.Game/Screens/Select/BeatmapClearScoresDialog.cs b/osu.Game/Screens/Select/BeatmapClearScoresDialog.cs
index 4970db8955..774d3b4b28 100644
--- a/osu.Game/Screens/Select/BeatmapClearScoresDialog.cs
+++ b/osu.Game/Screens/Select/BeatmapClearScoresDialog.cs
@@ -1,14 +1,13 @@
 // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
 // See the LICENCE file in the repository root for full licence text.
 
+using System;
+using System.Threading.Tasks;
 using osu.Framework.Allocation;
+using osu.Framework.Graphics.Sprites;
 using osu.Game.Beatmaps;
 using osu.Game.Overlays.Dialog;
 using osu.Game.Scoring;
-using System;
-using System.Linq;
-using System.Threading.Tasks;
-using osu.Framework.Graphics.Sprites;
 
 namespace osu.Game.Screens.Select
 {
@@ -19,7 +18,7 @@ namespace osu.Game.Screens.Select
 
         public BeatmapClearScoresDialog(BeatmapInfo beatmapInfo, Action onCompletion)
         {
-            BodyText = $@"{beatmapInfo.Metadata?.Artist} - {beatmapInfo.Metadata?.Title}";
+            BodyText = beatmapInfo.GetDisplayTitle();
             Icon = FontAwesome.Solid.Eraser;
             HeaderText = @"Clearing all local scores. Are you sure?";
             Buttons = new PopupDialogButton[]
@@ -29,7 +28,7 @@ namespace osu.Game.Screens.Select
                     Text = @"Yes. Please.",
                     Action = () =>
                     {
-                        Task.Run(() => scoreManager.Delete(scoreManager.QueryScores(s => !s.DeletePending && s.BeatmapInfo.ID == beatmapInfo.ID).ToList()))
+                        Task.Run(() => scoreManager.Delete(s => !s.DeletePending && s.BeatmapInfo.ID == beatmapInfo.ID))
                             .ContinueWith(_ => onCompletion);
                     }
                 },
diff --git a/osu.Game/Screens/Select/BeatmapDeleteDialog.cs b/osu.Game/Screens/Select/BeatmapDeleteDialog.cs
index 307c2352e3..1ac278d045 100644
--- a/osu.Game/Screens/Select/BeatmapDeleteDialog.cs
+++ b/osu.Game/Screens/Select/BeatmapDeleteDialog.cs
@@ -20,7 +20,7 @@ namespace osu.Game.Screens.Select
 
         public BeatmapDeleteDialog(BeatmapSetInfo beatmap)
         {
-            BodyText = $@"{beatmap.Metadata?.Artist} - {beatmap.Metadata?.Title}";
+            BodyText = $@"{beatmap.Metadata.Artist} - {beatmap.Metadata.Title}";
 
             Icon = FontAwesome.Regular.TrashAlt;
             HeaderText = @"Confirm deletion of";
diff --git a/osu.Game/Screens/Select/BeatmapDetailAreaTabControl.cs b/osu.Game/Screens/Select/BeatmapDetailAreaTabControl.cs
index df8c68a0dd..0fd39db97c 100644
--- a/osu.Game/Screens/Select/BeatmapDetailAreaTabControl.cs
+++ b/osu.Game/Screens/Select/BeatmapDetailAreaTabControl.cs
@@ -9,7 +9,6 @@ using osu.Framework.Bindables;
 using osu.Framework.Extensions.Color4Extensions;
 using osu.Framework.Graphics;
 using osu.Framework.Graphics.Containers;
-using osu.Game.Configuration;
 using osu.Game.Graphics;
 using osu.Game.Graphics.UserInterface;
 using osu.Framework.Graphics.Shapes;
@@ -83,7 +82,7 @@ namespace osu.Game.Screens.Select
         }
 
         [BackgroundDependencyLoader]
-        private void load(OsuColour colour, OsuConfigManager config)
+        private void load(OsuColour colour)
         {
             modsCheckbox.AccentColour = tabs.AccentColour = colour.YellowLight;
         }
diff --git a/osu.Game/Screens/Select/BeatmapInfoWedge.cs b/osu.Game/Screens/Select/BeatmapInfoWedge.cs
index 6791565828..ea531e89c8 100644
--- a/osu.Game/Screens/Select/BeatmapInfoWedge.cs
+++ b/osu.Game/Screens/Select/BeatmapInfoWedge.cs
@@ -184,7 +184,7 @@ namespace osu.Game.Screens.Select
             private void load(OsuColour colours, LocalisationManager localisation, BeatmapDifficultyCache difficultyCache)
             {
                 var beatmapInfo = working.BeatmapInfo;
-                var metadata = beatmapInfo.Metadata ?? working.BeatmapSetInfo?.Metadata ?? new BeatmapMetadata();
+                var metadata = beatmapInfo.Metadata;
 
                 RelativeSizeAxes = Axes.Both;
 
@@ -324,7 +324,7 @@ namespace osu.Game.Screens.Select
                 });
 
                 // no difficulty means it can't have a status to show
-                if (beatmapInfo.DifficultyName == null)
+                if (string.IsNullOrEmpty(beatmapInfo.DifficultyName))
                     StatusPill.Hide();
 
                 addInfoLabels();
diff --git a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs
index d2c7c75da8..d54a3bb54e 100644
--- a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs
+++ b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs
@@ -39,10 +39,10 @@ namespace osu.Game.Screens.Select.Carousel
             }
 
             match &= !criteria.StarDifficulty.HasFilter || criteria.StarDifficulty.IsInRange(BeatmapInfo.StarRating);
-            match &= !criteria.ApproachRate.HasFilter || criteria.ApproachRate.IsInRange(BeatmapInfo.BaseDifficulty.ApproachRate);
-            match &= !criteria.DrainRate.HasFilter || criteria.DrainRate.IsInRange(BeatmapInfo.BaseDifficulty.DrainRate);
-            match &= !criteria.CircleSize.HasFilter || criteria.CircleSize.IsInRange(BeatmapInfo.BaseDifficulty.CircleSize);
-            match &= !criteria.OverallDifficulty.HasFilter || criteria.OverallDifficulty.IsInRange(BeatmapInfo.BaseDifficulty.OverallDifficulty);
+            match &= !criteria.ApproachRate.HasFilter || criteria.ApproachRate.IsInRange(BeatmapInfo.Difficulty.ApproachRate);
+            match &= !criteria.DrainRate.HasFilter || criteria.DrainRate.IsInRange(BeatmapInfo.Difficulty.DrainRate);
+            match &= !criteria.CircleSize.HasFilter || criteria.CircleSize.IsInRange(BeatmapInfo.Difficulty.CircleSize);
+            match &= !criteria.OverallDifficulty.HasFilter || criteria.OverallDifficulty.IsInRange(BeatmapInfo.Difficulty.OverallDifficulty);
             match &= !criteria.Length.HasFilter || criteria.Length.IsInRange(BeatmapInfo.Length);
             match &= !criteria.BPM.HasFilter || criteria.BPM.IsInRange(BeatmapInfo.BPM);
 
diff --git a/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs
index 9e411d5daa..b2b3b5411c 100644
--- a/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs
+++ b/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs
@@ -39,6 +39,8 @@ namespace osu.Game.Screens.Select.Carousel
 
             beatmapSet.Beatmaps
                       .Where(b => !b.Hidden)
+                      .OrderBy(b => b.RulesetID)
+                      .ThenBy(b => b.StarRating)
                       .Select(b => new CarouselBeatmap(b))
                       .ForEach(AddChild);
         }
@@ -48,7 +50,7 @@ namespace osu.Game.Screens.Select.Carousel
             if (LastSelected == null || LastSelected.Filtered.Value)
             {
                 if (GetRecommendedBeatmap?.Invoke(Children.OfType<CarouselBeatmap>().Where(b => !b.Filtered.Value).Select(b => b.BeatmapInfo)) is BeatmapInfo recommended)
-                    return Children.OfType<CarouselBeatmap>().First(b => b.BeatmapInfo == recommended);
+                    return Children.OfType<CarouselBeatmap>().First(b => b.BeatmapInfo.Equals(recommended));
             }
 
             return base.GetNextToSelect();
diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs
index d0f9d835fd..3576b77ae8 100644
--- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs
+++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs
@@ -142,7 +142,7 @@ namespace osu.Game.Screens.Select.Carousel
                                         },
                                         new OsuSpriteText
                                         {
-                                            Text = $"{(beatmapInfo.Metadata ?? beatmapInfo.BeatmapSet.Metadata).Author.Username}",
+                                            Text = $"{beatmapInfo.Metadata.Author.Username}",
                                             Font = OsuFont.GetFont(italics: true),
                                             Anchor = Anchor.BottomLeft,
                                             Origin = Anchor.BottomLeft
@@ -159,7 +159,6 @@ namespace osu.Game.Screens.Select.Carousel
                                         new TopLocalRank(beatmapInfo)
                                         {
                                             Scale = new Vector2(0.8f),
-                                            Size = new Vector2(40, 20)
                                         },
                                         starCounter = new StarCounter
                                         {
@@ -238,8 +237,8 @@ namespace osu.Game.Screens.Select.Carousel
                 if (editRequested != null)
                     items.Add(new OsuMenuItem("Edit", MenuItemType.Standard, () => editRequested(beatmapInfo)));
 
-                if (beatmapInfo.OnlineID.HasValue && beatmapOverlay != null)
-                    items.Add(new OsuMenuItem("Details...", MenuItemType.Standard, () => beatmapOverlay.FetchAndShowBeatmap(beatmapInfo.OnlineID.Value)));
+                if (beatmapInfo.OnlineID > 0 && beatmapOverlay != null)
+                    items.Add(new OsuMenuItem("Details...", MenuItemType.Standard, () => beatmapOverlay.FetchAndShowBeatmap(beatmapInfo.OnlineID)));
 
                 if (collectionManager != null)
                 {
diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs
index 619b1e0fd0..618c5cf5ec 100644
--- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs
+++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs
@@ -61,7 +61,11 @@ namespace osu.Game.Screens.Select.Carousel
         [BackgroundDependencyLoader(true)]
         private void load(BeatmapSetOverlay beatmapOverlay)
         {
-            restoreHiddenRequested = s => s.Beatmaps.ForEach(manager.Restore);
+            restoreHiddenRequested = s =>
+            {
+                foreach (var b in s.Beatmaps)
+                    manager.Restore(b);
+            };
 
             if (beatmapOverlay != null)
                 viewDetails = beatmapOverlay.FetchAndShowBeatmapSet;
@@ -214,8 +218,8 @@ namespace osu.Game.Screens.Select.Carousel
                 if (Item.State.Value == CarouselItemState.NotSelected)
                     items.Add(new OsuMenuItem("Expand", MenuItemType.Highlighted, () => Item.State.Value = CarouselItemState.Selected));
 
-                if (beatmapSet.OnlineID != null && viewDetails != null)
-                    items.Add(new OsuMenuItem("Details...", MenuItemType.Standard, () => viewDetails(beatmapSet.OnlineID.Value)));
+                if (beatmapSet.OnlineID > 0 && viewDetails != null)
+                    items.Add(new OsuMenuItem("Details...", MenuItemType.Standard, () => viewDetails(beatmapSet.OnlineID)));
 
                 if (collectionManager != null)
                 {
diff --git a/osu.Game/Screens/Select/Carousel/SetPanelContent.cs b/osu.Game/Screens/Select/Carousel/SetPanelContent.cs
index f2054677b0..619806f96e 100644
--- a/osu.Game/Screens/Select/Carousel/SetPanelContent.cs
+++ b/osu.Game/Screens/Select/Carousel/SetPanelContent.cs
@@ -87,7 +87,8 @@ namespace osu.Game.Screens.Select.Carousel
             var beatmaps = carouselSet.Beatmaps.ToList();
 
             return beatmaps.Count > maximum_difficulty_icons
-                ? (IEnumerable<DifficultyIcon>)beatmaps.GroupBy(b => b.BeatmapInfo.Ruleset).Select(group => new FilterableGroupedDifficultyIcon(group.ToList(), group.Key))
+                ? (IEnumerable<DifficultyIcon>)beatmaps.GroupBy(b => b.BeatmapInfo.RulesetID)
+                                                       .Select(group => new FilterableGroupedDifficultyIcon(group.ToList(), group.Last().BeatmapInfo.Ruleset))
                 : beatmaps.Select(b => new FilterableDifficultyIcon(b));
         }
     }
diff --git a/osu.Game/Screens/Select/Carousel/TopLocalRank.cs b/osu.Game/Screens/Select/Carousel/TopLocalRank.cs
index 34129f232c..7ac99f4935 100644
--- a/osu.Game/Screens/Select/Carousel/TopLocalRank.cs
+++ b/osu.Game/Screens/Select/Carousel/TopLocalRank.cs
@@ -1,16 +1,20 @@
 // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
 // See the LICENCE file in the repository root for full licence text.
 
+using System;
 using System.Linq;
 using osu.Framework.Allocation;
 using osu.Framework.Bindables;
 using osu.Framework.Graphics;
-using osu.Framework.Threading;
 using osu.Game.Beatmaps;
+using osu.Game.Database;
+using osu.Game.Models;
 using osu.Game.Online.API;
 using osu.Game.Online.Leaderboards;
 using osu.Game.Rulesets;
 using osu.Game.Scoring;
+using osuTK;
+using Realms;
 
 namespace osu.Game.Screens.Select.Carousel
 {
@@ -18,74 +22,54 @@ namespace osu.Game.Screens.Select.Carousel
     {
         private readonly BeatmapInfo beatmapInfo;
 
-        [Resolved]
-        private ScoreManager scores { get; set; }
-
         [Resolved]
         private IBindable<RulesetInfo> ruleset { get; set; }
 
+        [Resolved]
+        private RealmContextFactory realmFactory { get; set; }
+
         [Resolved]
         private IAPIProvider api { get; set; }
 
+        private IDisposable scoreSubscription;
+
         public TopLocalRank(BeatmapInfo beatmapInfo)
             : base(null)
         {
             this.beatmapInfo = beatmapInfo;
+
+            Size = new Vector2(40, 20);
         }
 
-        [BackgroundDependencyLoader]
-        private void load()
+        protected override void LoadComplete()
         {
-            scores.ItemUpdated += scoreChanged;
-            scores.ItemRemoved += scoreChanged;
+            base.LoadComplete();
 
-            ruleset.ValueChanged += _ => fetchAndLoadTopScore();
-
-            fetchAndLoadTopScore();
-        }
-
-        private void scoreChanged(ScoreInfo score)
-        {
-            if (score.BeatmapInfoID == beatmapInfo.ID)
-                fetchAndLoadTopScore();
-        }
-
-        private ScheduledDelegate scheduledRankUpdate;
-
-        private void fetchAndLoadTopScore()
-        {
-            var rank = fetchTopScore()?.Rank;
-            scheduledRankUpdate = Schedule(() =>
+            ruleset.BindValueChanged(_ =>
             {
-                Rank = rank;
-
-                // Required since presence is changed via IsPresent override
-                Invalidate(Invalidation.Presence);
-            });
+                scoreSubscription?.Dispose();
+                scoreSubscription = realmFactory.Context.All<ScoreInfo>()
+                                                .Filter($"{nameof(ScoreInfo.User)}.{nameof(RealmUser.OnlineID)} == $0"
+                                                        + $" && {nameof(ScoreInfo.BeatmapInfo)}.{nameof(BeatmapInfo.ID)} == $1"
+                                                        + $" && {nameof(ScoreInfo.Ruleset)}.{nameof(RulesetInfo.ShortName)} == $2"
+                                                        + $" && {nameof(ScoreInfo.DeletePending)} == false", api.LocalUser.Value.Id, beatmapInfo.ID, ruleset.Value.ShortName)
+                                                .OrderByDescending(s => s.TotalScore)
+                                                .QueryAsyncWithNotifications((items, changes, ___) =>
+                                                {
+                                                    Rank = items.FirstOrDefault()?.Rank;
+                                                    // Required since presence is changed via IsPresent override
+                                                    Invalidate(Invalidation.Presence);
+                                                });
+            }, true);
         }
 
-        // We're present if a rank is set, or if there is a pending rank update (IsPresent = true is required for the scheduler to run).
-        public override bool IsPresent => base.IsPresent && (Rank != null || scheduledRankUpdate?.Completed == false);
-
-        private ScoreInfo fetchTopScore()
-        {
-            if (scores == null || beatmapInfo == null || ruleset?.Value == null || api?.LocalUser.Value == null)
-                return null;
-
-            return scores.QueryScores(s => s.UserID == api.LocalUser.Value.Id && s.BeatmapInfoID == beatmapInfo.ID && s.RulesetID == ruleset.Value.ID && !s.DeletePending)
-                         .OrderByDescending(s => s.TotalScore)
-                         .FirstOrDefault();
-        }
+        public override bool IsPresent => base.IsPresent && Rank != null;
 
         protected override void Dispose(bool isDisposing)
         {
             base.Dispose(isDisposing);
 
-            if (scores != null)
-            {
-                scores.ItemUpdated -= scoreChanged;
-                scores.ItemRemoved -= scoreChanged;
-            }
+            scoreSubscription?.Dispose();
         }
     }
 }
diff --git a/osu.Game/Screens/Select/FilterControl.cs b/osu.Game/Screens/Select/FilterControl.cs
index e95bd7f653..b53d64260a 100644
--- a/osu.Game/Screens/Select/FilterControl.cs
+++ b/osu.Game/Screens/Select/FilterControl.cs
@@ -2,7 +2,6 @@
 // See the LICENCE file in the repository root for full licence text.
 
 using System;
-using System.Diagnostics;
 using osu.Framework.Allocation;
 using osu.Framework.Bindables;
 using osu.Framework.Graphics;
@@ -37,8 +36,6 @@ namespace osu.Game.Screens.Select
 
         public FilterCriteria CreateCriteria()
         {
-            Debug.Assert(ruleset.Value.ID != null);
-
             string query = searchTextBox.Text;
 
             var criteria = new FilterCriteria
diff --git a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs
index 0102986070..49f2ea5d64 100644
--- a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs
+++ b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs
@@ -10,12 +10,14 @@ using osu.Framework.Allocation;
 using osu.Framework.Bindables;
 using osu.Framework.Extensions;
 using osu.Game.Beatmaps;
+using osu.Game.Database;
 using osu.Game.Online.API;
 using osu.Game.Online.API.Requests;
 using osu.Game.Online.Leaderboards;
 using osu.Game.Rulesets;
 using osu.Game.Rulesets.Mods;
 using osu.Game.Scoring;
+using Realms;
 
 namespace osu.Game.Screens.Select.Leaderboards
 {
@@ -26,6 +28,9 @@ namespace osu.Game.Screens.Select.Leaderboards
         [Resolved]
         private RulesetStore rulesets { get; set; }
 
+        [Resolved]
+        private RealmContextFactory realmFactory { get; set; }
+
         private BeatmapInfo beatmapInfo;
 
         public BeatmapInfo BeatmapInfo
@@ -33,13 +38,15 @@ namespace osu.Game.Screens.Select.Leaderboards
             get => beatmapInfo;
             set
             {
-                if (beatmapInfo == value)
+                if (beatmapInfo?.Equals(value) == true)
                     return;
 
                 beatmapInfo = value;
                 Scores = null;
 
                 UpdateScores();
+                if (IsLoaded)
+                    refreshRealmSubscription();
             }
         }
 
@@ -83,9 +90,34 @@ namespace osu.Game.Screens.Select.Leaderboards
                 if (filterMods)
                     UpdateScores();
             };
+        }
 
-            scoreManager.ItemRemoved += scoreStoreChanged;
-            scoreManager.ItemUpdated += scoreStoreChanged;
+        protected override void LoadComplete()
+        {
+            base.LoadComplete();
+
+            refreshRealmSubscription();
+        }
+
+        private IDisposable scoreSubscription;
+
+        private void refreshRealmSubscription()
+        {
+            scoreSubscription?.Dispose();
+            scoreSubscription = null;
+
+            if (beatmapInfo == null)
+                return;
+
+            scoreSubscription = realmFactory.Context.All<ScoreInfo>()
+                                            .Filter($"{nameof(ScoreInfo.BeatmapInfo)}.{nameof(BeatmapInfo.ID)} = $0", beatmapInfo.ID)
+                                            .QueryAsyncWithNotifications((_, changes, ___) =>
+                                            {
+                                                if (changes == null)
+                                                    return;
+
+                                                RefreshScores();
+                                            });
         }
 
         protected override void Reset()
@@ -94,17 +126,6 @@ namespace osu.Game.Screens.Select.Leaderboards
             TopScore = null;
         }
 
-        private void scoreStoreChanged(ScoreInfo score)
-        {
-            if (Scope != BeatmapLeaderboardScope.Local)
-                return;
-
-            if (BeatmapInfo?.ID != score.BeatmapInfoID)
-                return;
-
-            RefreshScores();
-        }
-
         protected override bool IsOnlineScope => Scope != BeatmapLeaderboardScope.Local;
 
         private CancellationTokenSource loadCancellationSource;
@@ -126,26 +147,33 @@ namespace osu.Game.Screens.Select.Leaderboards
 
             if (Scope == BeatmapLeaderboardScope.Local)
             {
-                var scores = scoreManager
-                    .QueryScores(s => !s.DeletePending && s.BeatmapInfo.ID == fetchBeatmapInfo.ID && s.Ruleset.ID == ruleset.Value.ID);
-
-                if (filterMods && !mods.Value.Any())
+                using (var realm = realmFactory.CreateContext())
                 {
-                    // we need to filter out all scores that have any mods to get all local nomod scores
-                    scores = scores.Where(s => !s.Mods.Any());
-                }
-                else if (filterMods)
-                {
-                    // otherwise find all the scores that have *any* of the currently selected mods (similar to how web applies mod filters)
-                    // we're creating and using a string list representation of selected mods so that it can be translated into the DB query itself
-                    var selectedMods = mods.Value.Select(m => m.Acronym);
-                    scores = scores.Where(s => s.Mods.Any(m => selectedMods.Contains(m.Acronym)));
-                }
+                    var scores = realm.All<ScoreInfo>()
+                                      .AsEnumerable()
+                                      // TODO: update to use a realm filter directly (or at least figure out the beatmap part to reduce scope).
+                                      .Where(s => !s.DeletePending && s.BeatmapInfo.ID == fetchBeatmapInfo.ID && s.Ruleset.OnlineID == ruleset.Value.ID);
 
-                scoreManager.OrderByTotalScoreAsync(scores.ToArray(), cancellationToken)
-                            .ContinueWith(task => scoresCallback?.Invoke(task.GetResultSafely()), TaskContinuationOptions.OnlyOnRanToCompletion);
+                    if (filterMods && !mods.Value.Any())
+                    {
+                        // we need to filter out all scores that have any mods to get all local nomod scores
+                        scores = scores.Where(s => !s.Mods.Any());
+                    }
+                    else if (filterMods)
+                    {
+                        // otherwise find all the scores that have *any* of the currently selected mods (similar to how web applies mod filters)
+                        // we're creating and using a string list representation of selected mods so that it can be translated into the DB query itself
+                        var selectedMods = mods.Value.Select(m => m.Acronym);
+                        scores = scores.Where(s => s.Mods.Any(m => selectedMods.Contains(m.Acronym)));
+                    }
 
-                return null;
+                    scores = scores.Detach();
+
+                    scoreManager.OrderByTotalScoreAsync(scores.ToArray(), cancellationToken)
+                                .ContinueWith(ordered => scoresCallback?.Invoke(ordered.GetResultSafely()), TaskContinuationOptions.OnlyOnRanToCompletion);
+
+                    return null;
+                }
             }
 
             if (api?.IsLoggedIn != true)
@@ -154,7 +182,7 @@ namespace osu.Game.Screens.Select.Leaderboards
                 return null;
             }
 
-            if (fetchBeatmapInfo.OnlineID == null || fetchBeatmapInfo.Status <= BeatmapOnlineStatus.Pending)
+            if (fetchBeatmapInfo.OnlineID <= 0 || fetchBeatmapInfo.Status <= BeatmapOnlineStatus.Pending)
             {
                 PlaceholderState = PlaceholderState.Unavailable;
                 return null;
@@ -206,11 +234,7 @@ namespace osu.Game.Screens.Select.Leaderboards
         {
             base.Dispose(isDisposing);
 
-            if (scoreManager != null)
-            {
-                scoreManager.ItemRemoved -= scoreStoreChanged;
-                scoreManager.ItemUpdated -= scoreStoreChanged;
-            }
+            scoreSubscription?.Dispose();
         }
     }
 }
diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs
index 08ad9f2ec0..837f30eb2b 100644
--- a/osu.Game/Screens/Select/SongSelect.cs
+++ b/osu.Game/Screens/Select/SongSelect.cs
@@ -295,7 +295,7 @@ namespace osu.Game.Screens.Select
                 Schedule(() =>
                 {
                     // if we have no beatmaps, let's prompt the user to import from over a stable install if he has one.
-                    if (!beatmaps.GetAllUsableBeatmapSetsEnumerable(IncludedDetails.Minimal).Any() && DisplayStableImportPrompt)
+                    if (beatmaps.QueryBeatmapSet(s => !s.Protected && !s.DeletePending) == null && DisplayStableImportPrompt)
                     {
                         dialogOverlay.Push(new ImportFromStablePopup(() =>
                         {
@@ -421,7 +421,7 @@ namespace osu.Game.Screens.Select
                 // A selection may not have been possible with filters applied.
 
                 // There was possibly a ruleset mismatch. This is a case we can help things along by updating the game-wide ruleset to match.
-                if (e.NewValue.BeatmapInfo.Ruleset != null && !e.NewValue.BeatmapInfo.Ruleset.Equals(decoupledRuleset.Value))
+                if (!e.NewValue.BeatmapInfo.Ruleset.Equals(decoupledRuleset.Value))
                 {
                     Ruleset.Value = e.NewValue.BeatmapInfo.Ruleset;
                     transferRulesetValue();
@@ -482,11 +482,11 @@ namespace osu.Game.Screens.Select
             else
                 selectionChangedDebounce = Scheduler.AddDelayed(run, 200);
 
-            if (beatmap != beatmapInfoPrevious)
+            if (beatmap?.Equals(beatmapInfoPrevious) != true)
             {
                 if (beatmap != null && beatmapInfoPrevious != null && Time.Current - audioFeedbackLastPlaybackTime >= 50)
                 {
-                    if (beatmap.BeatmapSetInfoID == beatmapInfoPrevious.BeatmapSetInfoID)
+                    if (beatmap.BeatmapSet?.ID == beatmapInfoPrevious.BeatmapSet?.ID)
                         sampleChangeDifficulty.Play();
                     else
                         sampleChangeBeatmap.Play();
@@ -807,14 +807,14 @@ namespace osu.Game.Screens.Select
 
         private void delete(BeatmapSetInfo beatmap)
         {
-            if (beatmap == null || !beatmap.IsManaged) return;
+            if (beatmap == null) return;
 
             dialogOverlay?.Push(new BeatmapDeleteDialog(beatmap));
         }
 
         private void clearScores(BeatmapInfo beatmapInfo)
         {
-            if (beatmapInfo == null || !beatmapInfo.IsManaged) return;
+            if (beatmapInfo == null) return;
 
             dialogOverlay?.Push(new BeatmapClearScoresDialog(beatmapInfo, () =>
                 // schedule done here rather than inside the dialog as the dialog may fade out and never callback.
diff --git a/osu.Game/Screens/Spectate/SpectatorScreen.cs b/osu.Game/Screens/Spectate/SpectatorScreen.cs
index c4e75cc413..dd586bdd37 100644
--- a/osu.Game/Screens/Spectate/SpectatorScreen.cs
+++ b/osu.Game/Screens/Spectate/SpectatorScreen.cs
@@ -1,6 +1,7 @@
 // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
 // See the LICENCE file in the repository root for full licence text.
 
+using System;
 using System.Collections.Generic;
 using System.Diagnostics;
 using System.Linq;
@@ -54,6 +55,11 @@ namespace osu.Game.Screens.Spectate
             this.users.AddRange(users);
         }
 
+        [Resolved]
+        private RealmContextFactory realmContextFactory { get; set; }
+
+        private IDisposable realmSubscription;
+
         protected override void LoadComplete()
         {
             base.LoadComplete();
@@ -73,7 +79,17 @@ namespace osu.Game.Screens.Spectate
                 playingUserStates.BindTo(spectatorClient.PlayingUserStates);
                 playingUserStates.BindCollectionChanged(onPlayingUserStatesChanged, true);
 
-                beatmaps.ItemUpdated += beatmapUpdated;
+                realmSubscription = realmContextFactory.Context
+                                                       .All<BeatmapSetInfo>()
+                                                       .Where(s => !s.DeletePending)
+                                                       .QueryAsyncWithNotifications((items, changes, ___) =>
+                                                       {
+                                                           if (changes?.InsertedIndices == null)
+                                                               return;
+
+                                                           foreach (int c in changes.InsertedIndices)
+                                                               beatmapUpdated(items[c]);
+                                                       });
 
                 foreach ((int id, var _) in userMap)
                     spectatorClient.WatchUser(id);
@@ -219,8 +235,7 @@ namespace osu.Game.Screens.Spectate
                     spectatorClient.StopWatchingUser(userId);
             }
 
-            if (beatmaps != null)
-                beatmaps.ItemUpdated -= beatmapUpdated;
+            realmSubscription?.Dispose();
         }
     }
 }
diff --git a/osu.Game/Skinning/LegacyBeatmapSkin.cs b/osu.Game/Skinning/LegacyBeatmapSkin.cs
index d44d3dce49..f80a980351 100644
--- a/osu.Game/Skinning/LegacyBeatmapSkin.cs
+++ b/osu.Game/Skinning/LegacyBeatmapSkin.cs
@@ -77,6 +77,6 @@ namespace osu.Game.Skinning
         }
 
         private static SkinInfo createSkinInfo(BeatmapInfo beatmapInfo) =>
-            new SkinInfo { Name = beatmapInfo.ToString(), Creator = beatmapInfo.Metadata?.Author.Username ?? string.Empty };
+            new SkinInfo { Name = beatmapInfo.ToString(), Creator = beatmapInfo.Metadata.Author.Username ?? string.Empty };
     }
 }
diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs
index e677e2c01b..359d9e5624 100644
--- a/osu.Game/Skinning/LegacySkin.cs
+++ b/osu.Game/Skinning/LegacySkin.cs
@@ -63,7 +63,7 @@ namespace osu.Game.Skinning
         /// <param name="resources">Access to raw game resources.</param>
         /// <param name="configurationFilename">The user-facing filename of the configuration file to be parsed. Can accept an .osu or skin.ini file.</param>
         protected LegacySkin(SkinInfo skin, [CanBeNull] IResourceStore<byte[]> storage, [CanBeNull] IStorageResourceProvider resources, string configurationFilename)
-            : this(skin, storage, resources, storage?.GetStream(configurationFilename))
+            : this(skin, storage, resources, string.IsNullOrEmpty(configurationFilename) ? null : storage?.GetStream(configurationFilename))
         {
         }
 
@@ -474,13 +474,18 @@ namespace osu.Game.Skinning
         {
             foreach (string name in getFallbackNames(componentName))
             {
+                // some component names (especially user-controlled ones, like `HitX` in mania)
+                // may contain `@2x` scale specifications.
+                // stable happens to check for that and strip them, so do the same to match stable behaviour.
+                string lookupName = name.Replace(@"@2x", string.Empty);
+
                 float ratio = 2;
-                var texture = Textures?.Get($"{name}@2x", wrapModeS, wrapModeT);
+                var texture = Textures?.Get(@$"{lookupName}@2x", wrapModeS, wrapModeT);
 
                 if (texture == null)
                 {
                     ratio = 1;
-                    texture = Textures?.Get(name, wrapModeS, wrapModeT);
+                    texture = Textures?.Get(lookupName, wrapModeS, wrapModeT);
                 }
 
                 if (texture == null)
diff --git a/osu.Game/Skinning/SkinModelManager.cs b/osu.Game/Skinning/SkinModelManager.cs
index 964d99a2e5..b8313f63a3 100644
--- a/osu.Game/Skinning/SkinModelManager.cs
+++ b/osu.Game/Skinning/SkinModelManager.cs
@@ -7,7 +7,6 @@ using System.IO;
 using System.Linq;
 using System.Text;
 using System.Threading;
-using System.Threading.Tasks;
 using Newtonsoft.Json;
 using osu.Framework.Logging;
 using osu.Framework.Platform;
@@ -49,7 +48,7 @@ namespace osu.Game.Skinning
 
         protected override bool HasCustomHashFunction => true;
 
-        protected override Task Populate(SkinInfo model, ArchiveReader? archive, Realm realm, CancellationToken cancellationToken = default)
+        protected override void Populate(SkinInfo model, ArchiveReader? archive, Realm realm, CancellationToken cancellationToken = default)
         {
             var skinInfoFile = model.Files.SingleOrDefault(f => f.Filename == skin_info_file);
 
@@ -83,8 +82,6 @@ namespace osu.Game.Skinning
             model.InstantiationInfo = createInstance(model).GetType().GetInvariantInstantiationInfo();
 
             checkSkinIniMetadata(model, realm);
-
-            return Task.CompletedTask;
         }
 
         private void checkSkinIniMetadata(SkinInfo item, Realm realm)
@@ -262,5 +259,7 @@ namespace osu.Game.Skinning
                 s.Hash = ComputeHash(s);
             });
         }
+
+        public override bool IsAvailableLocally(SkinInfo model) => true; // skins do not have online download support yet.
     }
 }
diff --git a/osu.Game/Skinning/SkinStore.cs b/osu.Game/Skinning/SkinStore.cs
deleted file mode 100644
index 922d146259..0000000000
--- a/osu.Game/Skinning/SkinStore.cs
+++ /dev/null
@@ -1,16 +0,0 @@
-// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using osu.Framework.Platform;
-using osu.Game.Database;
-
-namespace osu.Game.Skinning
-{
-    public class SkinStore : MutableDatabaseBackedStoreWithFileIncludes<EFSkinInfo, SkinFileInfo>
-    {
-        public SkinStore(DatabaseContextFactory contextFactory, Storage storage = null)
-            : base(contextFactory, storage)
-        {
-        }
-    }
-}
diff --git a/osu.Game/Stores/BeatmapImporter.cs b/osu.Game/Stores/BeatmapImporter.cs
index 8ab6941885..d285a6b61c 100644
--- a/osu.Game/Stores/BeatmapImporter.cs
+++ b/osu.Game/Stores/BeatmapImporter.cs
@@ -6,7 +6,6 @@ using System.Collections.Generic;
 using System.IO;
 using System.Linq;
 using System.Threading;
-using System.Threading.Tasks;
 using osu.Framework.Audio.Track;
 using osu.Framework.Extensions;
 using osu.Framework.Extensions.IEnumerableExtensions;
@@ -34,7 +33,7 @@ namespace osu.Game.Stores
     /// Handles the storage and retrieval of Beatmaps/WorkingBeatmaps.
     /// </summary>
     [ExcludeFromDynamicCompile]
-    public class BeatmapImporter : RealmArchiveModelImporter<RealmBeatmapSet>, IDisposable
+    public abstract class BeatmapImporter : RealmArchiveModelManager<BeatmapSetInfo>, IDisposable
     {
         public override IEnumerable<string> HandledExtensions => new[] { ".osz" };
 
@@ -45,7 +44,7 @@ namespace osu.Game.Stores
 
         private readonly BeatmapOnlineLookupQueue? onlineLookupQueue;
 
-        public BeatmapImporter(RealmContextFactory contextFactory, Storage storage, BeatmapOnlineLookupQueue? onlineLookupQueue = null)
+        protected BeatmapImporter(RealmContextFactory contextFactory, Storage storage, BeatmapOnlineLookupQueue? onlineLookupQueue = null)
             : base(storage, contextFactory)
         {
             this.onlineLookupQueue = onlineLookupQueue;
@@ -53,23 +52,26 @@ namespace osu.Game.Stores
 
         protected override bool ShouldDeleteArchive(string path) => Path.GetExtension(path).ToLowerInvariant() == ".osz";
 
-        protected override Task Populate(RealmBeatmapSet beatmapSet, ArchiveReader? archive, Realm realm, CancellationToken cancellationToken = default)
+        protected override void Populate(BeatmapSetInfo beatmapSet, ArchiveReader? archive, Realm realm, CancellationToken cancellationToken = default)
         {
             if (archive != null)
                 beatmapSet.Beatmaps.AddRange(createBeatmapDifficulties(beatmapSet.Files, realm));
 
-            foreach (RealmBeatmap b in beatmapSet.Beatmaps)
+            foreach (BeatmapInfo b in beatmapSet.Beatmaps)
+            {
                 b.BeatmapSet = beatmapSet;
 
+                // ensure we aren't trying to add a new ruleset to the database
+                // this can happen in tests, mostly
+                if (!b.Ruleset.IsManaged)
+                    b.Ruleset = realm.Find<RulesetInfo>(b.Ruleset.ShortName) ?? throw new ArgumentNullException(nameof(b.Ruleset));
+            }
+
             validateOnlineIds(beatmapSet, realm);
 
             bool hadOnlineIDs = beatmapSet.Beatmaps.Any(b => b.OnlineID > 0);
 
-            if (onlineLookupQueue != null)
-            {
-                // TODO: this required `BeatmapOnlineLookupQueue` to somehow support new types.
-                // await onlineLookupQueue.UpdateAsync(beatmapSet, cancellationToken).ConfigureAwait(false);
-            }
+            onlineLookupQueue?.Update(beatmapSet);
 
             // ensure at least one beatmap was able to retrieve or keep an online ID, else drop the set ID.
             if (hadOnlineIDs && !beatmapSet.Beatmaps.Any(b => b.OnlineID > 0))
@@ -80,11 +82,9 @@ namespace osu.Game.Stores
                     LogForModel(beatmapSet, "Disassociating beatmap set ID due to loss of all beatmap IDs");
                 }
             }
-
-            return Task.CompletedTask;
         }
 
-        protected override void PreImport(RealmBeatmapSet beatmapSet, Realm realm)
+        protected override void PreImport(BeatmapSetInfo beatmapSet, Realm realm)
         {
             // We are about to import a new beatmap. Before doing so, ensure that no other set shares the online IDs used by the new one.
             // Note that this means if the previous beatmap is restored by the user, it will no longer be linked to its online IDs.
@@ -93,7 +93,7 @@ namespace osu.Game.Stores
 
             if (beatmapSet.OnlineID > 0)
             {
-                var existingSetWithSameOnlineID = realm.All<RealmBeatmapSet>().SingleOrDefault(b => b.OnlineID == beatmapSet.OnlineID);
+                var existingSetWithSameOnlineID = realm.All<BeatmapSetInfo>().SingleOrDefault(b => b.OnlineID == beatmapSet.OnlineID);
 
                 if (existingSetWithSameOnlineID != null)
                 {
@@ -108,7 +108,7 @@ namespace osu.Game.Stores
             }
         }
 
-        private void validateOnlineIds(RealmBeatmapSet beatmapSet, Realm realm)
+        private void validateOnlineIds(BeatmapSetInfo beatmapSet, Realm realm)
         {
             var beatmapIds = beatmapSet.Beatmaps.Where(b => b.OnlineID > 0).Select(b => b.OnlineID).ToList();
 
@@ -121,10 +121,10 @@ namespace osu.Game.Stores
             }
 
             // find any existing beatmaps in the database that have matching online ids
-            List<RealmBeatmap> existingBeatmaps = new List<RealmBeatmap>();
+            List<BeatmapInfo> existingBeatmaps = new List<BeatmapInfo>();
 
             foreach (int id in beatmapIds)
-                existingBeatmaps.AddRange(realm.All<RealmBeatmap>().Where(b => b.OnlineID == id));
+                existingBeatmaps.AddRange(realm.All<BeatmapInfo>().Where(b => b.OnlineID == id));
 
             if (existingBeatmaps.Any())
             {
@@ -143,7 +143,7 @@ namespace osu.Game.Stores
             void resetIds() => beatmapSet.Beatmaps.ForEach(b => b.OnlineID = -1);
         }
 
-        protected override bool CanSkipImport(RealmBeatmapSet existing, RealmBeatmapSet import)
+        protected override bool CanSkipImport(BeatmapSetInfo existing, BeatmapSetInfo import)
         {
             if (!base.CanSkipImport(existing, import))
                 return false;
@@ -151,7 +151,7 @@ namespace osu.Game.Stores
             return existing.Beatmaps.Any(b => b.OnlineID > 0);
         }
 
-        protected override bool CanReuseExisting(RealmBeatmapSet existing, RealmBeatmapSet import)
+        protected override bool CanReuseExisting(BeatmapSetInfo existing, BeatmapSetInfo import)
         {
             if (!base.CanReuseExisting(existing, import))
                 return false;
@@ -163,9 +163,15 @@ namespace osu.Game.Stores
             return existing.OnlineID == import.OnlineID && existingIds.SequenceEqual(importIds);
         }
 
+        public override bool IsAvailableLocally(BeatmapSetInfo model)
+        {
+            using (var context = ContextFactory.CreateContext())
+                return context.All<BeatmapInfo>().Any(b => b.OnlineID == model.OnlineID);
+        }
+
         public override string HumanisedModelName => "beatmap";
 
-        protected override RealmBeatmapSet? CreateModel(ArchiveReader reader)
+        protected override BeatmapSetInfo? CreateModel(ArchiveReader reader)
         {
             // let's make sure there are actually .osu files to import.
             string? mapName = reader.Filenames.FirstOrDefault(f => f.EndsWith(".osu", StringComparison.OrdinalIgnoreCase));
@@ -180,7 +186,7 @@ namespace osu.Game.Stores
             using (var stream = new LineBufferedReader(reader.GetStream(mapName)))
                 beatmap = Decoder.GetDecoder<Beatmap>(stream).Decode(stream);
 
-            return new RealmBeatmapSet
+            return new BeatmapSetInfo
             {
                 OnlineID = beatmap.BeatmapInfo.BeatmapSet?.OnlineID ?? -1,
                 // Metadata = beatmap.Metadata,
@@ -189,11 +195,11 @@ namespace osu.Game.Stores
         }
 
         /// <summary>
-        /// Create all required <see cref="RealmBeatmap"/>s for the provided archive.
+        /// Create all required <see cref="BeatmapInfo"/>s for the provided archive.
         /// </summary>
-        private List<RealmBeatmap> createBeatmapDifficulties(IList<RealmNamedFileUsage> files, Realm realm)
+        private List<BeatmapInfo> createBeatmapDifficulties(IList<RealmNamedFileUsage> files, Realm realm)
         {
-            var beatmaps = new List<RealmBeatmap>();
+            var beatmaps = new List<BeatmapInfo>();
 
             foreach (var file in files.Where(f => f.Filename.EndsWith(".osu", StringComparison.OrdinalIgnoreCase)))
             {
@@ -212,9 +218,9 @@ namespace osu.Game.Stores
                     }
 
                     var decodedInfo = decoded.BeatmapInfo;
-                    var decodedDifficulty = decodedInfo.BaseDifficulty;
+                    var decodedDifficulty = decodedInfo.Difficulty;
 
-                    var ruleset = realm.All<RealmRuleset>().FirstOrDefault(r => r.OnlineID == decodedInfo.RulesetID);
+                    var ruleset = realm.All<RulesetInfo>().FirstOrDefault(r => r.OnlineID == decodedInfo.RulesetID);
 
                     if (ruleset?.Available != true)
                     {
@@ -222,7 +228,7 @@ namespace osu.Game.Stores
                         continue;
                     }
 
-                    var difficulty = new RealmBeatmapDifficulty
+                    var difficulty = new BeatmapDifficulty
                     {
                         DrainRate = decodedDifficulty.DrainRate,
                         CircleSize = decodedDifficulty.CircleSize,
@@ -232,7 +238,7 @@ namespace osu.Game.Stores
                         SliderTickRate = decodedDifficulty.SliderTickRate,
                     };
 
-                    var metadata = new RealmBeatmapMetadata
+                    var metadata = new BeatmapMetadata
                     {
                         Title = decoded.Metadata.Title,
                         TitleUnicode = decoded.Metadata.TitleUnicode,
@@ -240,7 +246,7 @@ namespace osu.Game.Stores
                         ArtistUnicode = decoded.Metadata.ArtistUnicode,
                         Author =
                         {
-                            OnlineID = decoded.Metadata.Author.Id,
+                            OnlineID = decoded.Metadata.Author.OnlineID,
                             Username = decoded.Metadata.Author.Username
                         },
                         Source = decoded.Metadata.Source,
@@ -250,11 +256,11 @@ namespace osu.Game.Stores
                         BackgroundFile = decoded.Metadata.BackgroundFile,
                     };
 
-                    var beatmap = new RealmBeatmap(ruleset, difficulty, metadata)
+                    var beatmap = new BeatmapInfo(ruleset, difficulty, metadata)
                     {
                         Hash = hash,
                         DifficultyName = decodedInfo.DifficultyName,
-                        OnlineID = decodedInfo.OnlineID ?? -1,
+                        OnlineID = decodedInfo.OnlineID,
                         AudioLeadIn = decodedInfo.AudioLeadIn,
                         StackLeniency = decodedInfo.StackLeniency,
                         SpecialStyle = decodedInfo.SpecialStyle,
@@ -278,7 +284,7 @@ namespace osu.Game.Stores
             return beatmaps;
         }
 
-        private void updateBeatmapStatistics(RealmBeatmap beatmap, IBeatmap decoded)
+        private void updateBeatmapStatistics(BeatmapInfo beatmap, IBeatmap decoded)
         {
             var rulesetInstance = ((IRulesetInfo)beatmap.Ruleset).CreateInstance();
 
diff --git a/osu.Game/Stores/RealmArchiveModelImporter.cs b/osu.Game/Stores/RealmArchiveModelImporter.cs
index 4aca079e2e..2ea7aecc94 100644
--- a/osu.Game/Stores/RealmArchiveModelImporter.cs
+++ b/osu.Game/Stores/RealmArchiveModelImporter.cs
@@ -169,7 +169,7 @@ namespace osu.Game.Stores
             else
             {
                 notification.CompletionText = imported.Count == 1
-                    ? $"Imported {imported.First()}!"
+                    ? $"Imported {imported.First().GetDisplayString()}!"
                     : $"Imported {imported.Count} {HumanisedModelName}s!";
 
                 if (imported.Count > 0 && PostImport != null)
@@ -318,7 +318,7 @@ namespace osu.Game.Stores
         /// <param name="archive">An optional archive to use for model population.</param>
         /// <param name="lowPriority">Whether this is a low priority import.</param>
         /// <param name="cancellationToken">An optional cancellation token.</param>
-        public virtual async Task<ILive<TModel>?> Import(TModel item, ArchiveReader? archive = null, bool lowPriority = false, CancellationToken cancellationToken = default)
+        public virtual Task<ILive<TModel>?> Import(TModel item, ArchiveReader? archive = null, bool lowPriority = false, CancellationToken cancellationToken = default)
         {
             using (var realm = ContextFactory.CreateContext())
             {
@@ -352,7 +352,7 @@ namespace osu.Game.Stores
                                 transaction.Commit();
                             }
 
-                            return existing.ToLive(ContextFactory);
+                            return Task.FromResult((ILive<TModel>?)existing.ToLive(ContextFactory));
                         }
 
                         LogForModel(item, @"Found existing (optimised) but failed pre-check.");
@@ -373,7 +373,7 @@ namespace osu.Game.Stores
                         item.Hash = ComputeHash(item);
 
                         // TODO: we may want to run this outside of the transaction.
-                        await Populate(item, archive, realm, cancellationToken).ConfigureAwait(false);
+                        Populate(item, archive, realm, cancellationToken);
 
                         if (!checkedExisting)
                             existing = CheckForExisting(item, realm);
@@ -387,15 +387,12 @@ namespace osu.Game.Stores
                                 existing.DeletePending = false;
                                 transaction.Commit();
 
-                                return existing.ToLive(ContextFactory);
+                                return Task.FromResult((ILive<TModel>?)existing.ToLive(ContextFactory));
                             }
 
                             LogForModel(item, @"Found existing but failed re-use check.");
 
                             existing.DeletePending = true;
-
-                            // todo: actually delete? i don't think this is required...
-                            // ModelStore.PurgeDeletable(s => s.ID == existing.ID);
                         }
 
                         PreImport(item, realm);
@@ -416,7 +413,7 @@ namespace osu.Game.Stores
                     throw;
                 }
 
-                return item.ToLive(ContextFactory);
+                return Task.FromResult((ILive<TModel>?)item.ToLive(ContextFactory));
             }
         }
 
@@ -483,7 +480,7 @@ namespace osu.Game.Stores
         /// <param name="archive">The archive to use as a reference for population. May be null.</param>
         /// <param name="realm">The current realm context.</param>
         /// <param name="cancellationToken">An optional cancellation token.</param>
-        protected abstract Task Populate(TModel model, ArchiveReader? archive, Realm realm, CancellationToken cancellationToken = default);
+        protected abstract void Populate(TModel model, ArchiveReader? archive, Realm realm, CancellationToken cancellationToken = default);
 
         /// <summary>
         /// Perform any final actions before the import to database executes.
diff --git a/osu.Game/Stores/RealmArchiveModelManager.cs b/osu.Game/Stores/RealmArchiveModelManager.cs
index 87a27cbbbc..b456dae343 100644
--- a/osu.Game/Stores/RealmArchiveModelManager.cs
+++ b/osu.Game/Stores/RealmArchiveModelManager.cs
@@ -5,11 +5,9 @@ using System;
 using System.Collections.Generic;
 using System.IO;
 using System.Linq;
-using System.Threading;
-using System.Threading.Tasks;
 using osu.Framework.Platform;
 using osu.Game.Database;
-using osu.Game.IO.Archives;
+using osu.Game.Extensions;
 using osu.Game.Models;
 using osu.Game.Overlays.Notifications;
 using Realms;
@@ -19,27 +17,11 @@ using Realms;
 namespace osu.Game.Stores
 {
     /// <summary>
-    /// Class which adds all the missing pieces bridging the gap between <see cref="RealmArchiveModelImporter{TModel}"/> and <see cref="ArchiveModelManager{TModel,TFileModel}"/>.
+    /// Class which adds all the missing pieces bridging the gap between <see cref="RealmArchiveModelImporter{TModel}"/> and (legacy) ArchiveModelManager.
     /// </summary>
     public abstract class RealmArchiveModelManager<TModel> : RealmArchiveModelImporter<TModel>, IModelManager<TModel>, IModelFileManager<TModel, RealmNamedFileUsage>
         where TModel : RealmObject, IHasRealmFiles, IHasGuidPrimaryKey, ISoftDelete
     {
-        public event Action<TModel>? ItemUpdated
-        {
-            // This may be brought back for beatmaps to ease integration.
-            // The eventual goal would be not requiring this and using realm subscriptions in its place.
-            add => throw new NotImplementedException();
-            remove => throw new NotImplementedException();
-        }
-
-        public event Action<TModel>? ItemRemoved
-        {
-            // This may be brought back for beatmaps to ease integration.
-            // The eventual goal would be not requiring this and using realm subscriptions in its place.
-            add => throw new NotImplementedException();
-            remove => throw new NotImplementedException();
-        }
-
         private readonly RealmFileStore realmFileStore;
 
         protected RealmArchiveModelManager(Storage storage, RealmContextFactory contextFactory)
@@ -49,13 +31,29 @@ namespace osu.Game.Stores
         }
 
         public void DeleteFile(TModel item, RealmNamedFileUsage file) =>
-            item.Realm.Write(() => DeleteFile(item, file, item.Realm));
+            performFileOperation(item, managed => DeleteFile(managed, managed.Files.First(f => f.Filename == file.Filename), managed.Realm));
 
-        public void ReplaceFile(TModel item, RealmNamedFileUsage file, Stream contents)
-            => item.Realm.Write(() => ReplaceFile(file, contents, item.Realm));
+        public void ReplaceFile(TModel item, RealmNamedFileUsage file, Stream contents) =>
+            performFileOperation(item, managed => ReplaceFile(file, contents, managed.Realm));
 
-        public void AddFile(TModel item, Stream contents, string filename)
-            => item.Realm.Write(() => AddFile(item, contents, filename, item.Realm));
+        public void AddFile(TModel item, Stream contents, string filename) =>
+            performFileOperation(item, managed => AddFile(managed, contents, filename, managed.Realm));
+
+        private void performFileOperation(TModel item, Action<TModel> operation)
+        {
+            // While we are detaching so often, this seems like the easiest way to keep things in sync.
+            // This method should be removed as soon as all the surrounding pieces support non-detached operations.
+            if (!item.IsManaged)
+            {
+                var managed = ContextFactory.Context.Find<TModel>(item.ID);
+                managed.Realm.Write(() => operation(managed));
+
+                item.Files.Clear();
+                item.Files.AddRange(managed.Files.Detach());
+            }
+            else
+                operation(item);
+        }
 
         /// <summary>
         /// Delete a file from within an ongoing realm transaction.
@@ -92,11 +90,6 @@ namespace osu.Game.Stores
             item.Files.Add(namedUsage);
         }
 
-        public override async Task<ILive<TModel>?> Import(TModel item, ArchiveReader? archive = null, bool lowPriority = false, CancellationToken cancellationToken = default)
-        {
-            return await base.Import(item, archive, lowPriority, cancellationToken).ConfigureAwait(false);
-        }
-
         /// <summary>
         /// Delete multiple items.
         /// This will post notifications tracking progress.
@@ -172,25 +165,33 @@ namespace osu.Game.Stores
 
         public bool Delete(TModel item)
         {
-            if (item.DeletePending)
-                return false;
+            using (var realm = ContextFactory.CreateContext())
+            {
+                if (!item.IsManaged)
+                    item = realm.Find<TModel>(item.ID);
 
-            item.Realm.Write(r => item.DeletePending = true);
-            return true;
+                if (item?.DeletePending != false)
+                    return false;
+
+                realm.Write(r => item.DeletePending = true);
+                return true;
+            }
         }
 
         public void Undelete(TModel item)
         {
-            if (!item.DeletePending)
-                return;
+            using (var realm = ContextFactory.CreateContext())
+            {
+                if (!item.IsManaged)
+                    item = realm.Find<TModel>(item.ID);
 
-            item.Realm.Write(r => item.DeletePending = false);
+                if (item?.DeletePending != true)
+                    return;
+
+                realm.Write(r => item.DeletePending = false);
+            }
         }
 
-        public virtual bool IsAvailableLocally(TModel model) => false; // Not relevant for skins since they can't be downloaded yet.
-
-        public void Update(TModel skin)
-        {
-        }
+        public abstract bool IsAvailableLocally(TModel model);
     }
 }
diff --git a/osu.Game/Stores/RealmRulesetStore.cs b/osu.Game/Stores/RealmRulesetStore.cs
deleted file mode 100644
index 93b6d29e7d..0000000000
--- a/osu.Game/Stores/RealmRulesetStore.cs
+++ /dev/null
@@ -1,270 +0,0 @@
-// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Linq;
-using System.Reflection;
-using osu.Framework;
-using osu.Framework.Extensions.ObjectExtensions;
-using osu.Framework.Logging;
-using osu.Framework.Platform;
-using osu.Game.Database;
-using osu.Game.Models;
-using osu.Game.Rulesets;
-
-#nullable enable
-
-namespace osu.Game.Stores
-{
-    public class RealmRulesetStore : IRulesetStore, IDisposable
-    {
-        private readonly RealmContextFactory realmFactory;
-
-        private const string ruleset_library_prefix = @"osu.Game.Rulesets";
-
-        private readonly Dictionary<Assembly, Type> loadedAssemblies = new Dictionary<Assembly, Type>();
-
-        /// <summary>
-        /// All available rulesets.
-        /// </summary>
-        public IEnumerable<RealmRuleset> AvailableRulesets => availableRulesets;
-
-        private readonly List<RealmRuleset> availableRulesets = new List<RealmRuleset>();
-
-        public RealmRulesetStore(RealmContextFactory realmFactory, Storage? storage = null)
-        {
-            this.realmFactory = realmFactory;
-
-            // On android in release configuration assemblies are loaded from the apk directly into memory.
-            // We cannot read assemblies from cwd, so should check loaded assemblies instead.
-            loadFromAppDomain();
-
-            // This null check prevents Android from attempting to load the rulesets from disk,
-            // as the underlying path "AppContext.BaseDirectory", despite being non-nullable, it returns null on android.
-            // See https://github.com/xamarin/xamarin-android/issues/3489.
-            if (RuntimeInfo.StartupDirectory != null)
-                loadFromDisk();
-
-            // the event handler contains code for resolving dependency on the game assembly for rulesets located outside the base game directory.
-            // It needs to be attached to the assembly lookup event before the actual call to loadUserRulesets() else rulesets located out of the base game directory will fail
-            // to load as unable to locate the game core assembly.
-            AppDomain.CurrentDomain.AssemblyResolve += resolveRulesetDependencyAssembly;
-
-            var rulesetStorage = storage?.GetStorageForDirectory(@"rulesets");
-            if (rulesetStorage != null)
-                loadUserRulesets(rulesetStorage);
-
-            addMissingRulesets();
-        }
-
-        /// <summary>
-        /// Retrieve a ruleset using a known ID.
-        /// </summary>
-        /// <param name="id">The ruleset's internal ID.</param>
-        /// <returns>A ruleset, if available, else null.</returns>
-        public RealmRuleset? GetRuleset(int id) => AvailableRulesets.FirstOrDefault(r => r.OnlineID == id);
-
-        /// <summary>
-        /// Retrieve a ruleset using a known short name.
-        /// </summary>
-        /// <param name="shortName">The ruleset's short name.</param>
-        /// <returns>A ruleset, if available, else null.</returns>
-        public RealmRuleset? GetRuleset(string shortName) => AvailableRulesets.FirstOrDefault(r => r.ShortName == shortName);
-
-        private Assembly? resolveRulesetDependencyAssembly(object? sender, ResolveEventArgs args)
-        {
-            var asm = new AssemblyName(args.Name);
-
-            // the requesting assembly may be located out of the executable's base directory, thus requiring manual resolving of its dependencies.
-            // this attempts resolving the ruleset dependencies on game core and framework assemblies by returning assemblies with the same assembly name
-            // already loaded in the AppDomain.
-            var domainAssembly = AppDomain.CurrentDomain.GetAssemblies()
-                                          // Given name is always going to be equally-or-more qualified than the assembly name.
-                                          .Where(a =>
-                                          {
-                                              string? name = a.GetName().Name;
-                                              if (name == null)
-                                                  return false;
-
-                                              return args.Name.Contains(name, StringComparison.Ordinal);
-                                          })
-                                          // Pick the greatest assembly version.
-                                          .OrderByDescending(a => a.GetName().Version)
-                                          .FirstOrDefault();
-
-            if (domainAssembly != null)
-                return domainAssembly;
-
-            return loadedAssemblies.Keys.FirstOrDefault(a => a.FullName == asm.FullName);
-        }
-
-        private void addMissingRulesets()
-        {
-            using (var context = realmFactory.CreateContext())
-            {
-                context.Write(realm =>
-                {
-                    var rulesets = realm.All<RealmRuleset>();
-
-                    List<Ruleset> instances = loadedAssemblies.Values
-                                                              .Select(r => Activator.CreateInstance(r) as Ruleset)
-                                                              .Where(r => r != null)
-                                                              .Select(r => r.AsNonNull())
-                                                              .ToList();
-
-                    // add all legacy rulesets first to ensure they have exclusive choice of primary key.
-                    foreach (var r in instances.Where(r => r is ILegacyRuleset))
-                    {
-                        if (realm.All<RealmRuleset>().FirstOrDefault(rr => rr.OnlineID == r.RulesetInfo.OnlineID) == null)
-                            realm.Add(new RealmRuleset(r.RulesetInfo.ShortName, r.RulesetInfo.Name, r.RulesetInfo.InstantiationInfo, r.RulesetInfo.OnlineID));
-                    }
-
-                    // add any other rulesets which have assemblies present but are not yet in the database.
-                    foreach (var r in instances.Where(r => !(r is ILegacyRuleset)))
-                    {
-                        if (rulesets.FirstOrDefault(ri => ri.InstantiationInfo.Equals(r.RulesetInfo.InstantiationInfo, StringComparison.Ordinal)) == null)
-                        {
-                            var existingSameShortName = rulesets.FirstOrDefault(ri => ri.ShortName == r.RulesetInfo.ShortName);
-
-                            if (existingSameShortName != null)
-                            {
-                                // even if a matching InstantiationInfo was not found, there may be an existing ruleset with the same ShortName.
-                                // this generally means the user or ruleset provider has renamed their dll but the underlying ruleset is *likely* the same one.
-                                // in such cases, update the instantiation info of the existing entry to point to the new one.
-                                existingSameShortName.InstantiationInfo = r.RulesetInfo.InstantiationInfo;
-                            }
-                            else
-                                realm.Add(new RealmRuleset(r.RulesetInfo.ShortName, r.RulesetInfo.Name, r.RulesetInfo.InstantiationInfo, r.RulesetInfo.OnlineID));
-                        }
-                    }
-
-                    List<RealmRuleset> detachedRulesets = new List<RealmRuleset>();
-
-                    // perform a consistency check and detach final rulesets from realm for cross-thread runtime usage.
-                    foreach (var r in rulesets)
-                    {
-                        try
-                        {
-                            var resolvedType = Type.GetType(r.InstantiationInfo)
-                                               ?? throw new RulesetLoadException(@"Type could not be resolved");
-
-                            var instanceInfo = (Activator.CreateInstance(resolvedType) as Ruleset)?.RulesetInfo
-                                               ?? throw new RulesetLoadException(@"Instantiation failure");
-
-                            r.Name = instanceInfo.Name;
-                            r.ShortName = instanceInfo.ShortName;
-                            r.InstantiationInfo = instanceInfo.InstantiationInfo;
-                            r.Available = true;
-
-                            detachedRulesets.Add(r.Clone());
-                        }
-                        catch (Exception ex)
-                        {
-                            r.Available = false;
-                            Logger.Log($"Could not load ruleset {r}: {ex.Message}");
-                        }
-                    }
-
-                    availableRulesets.AddRange(detachedRulesets);
-                });
-            }
-        }
-
-        private void loadFromAppDomain()
-        {
-            foreach (var ruleset in AppDomain.CurrentDomain.GetAssemblies())
-            {
-                string? rulesetName = ruleset.GetName().Name;
-
-                if (rulesetName == null)
-                    continue;
-
-                if (!rulesetName.StartsWith(ruleset_library_prefix, StringComparison.InvariantCultureIgnoreCase) || rulesetName.Contains(@"Tests"))
-                    continue;
-
-                addRuleset(ruleset);
-            }
-        }
-
-        private void loadUserRulesets(Storage rulesetStorage)
-        {
-            var rulesets = rulesetStorage.GetFiles(@".", @$"{ruleset_library_prefix}.*.dll");
-
-            foreach (string? ruleset in rulesets.Where(f => !f.Contains(@"Tests")))
-                loadRulesetFromFile(rulesetStorage.GetFullPath(ruleset));
-        }
-
-        private void loadFromDisk()
-        {
-            try
-            {
-                string[] files = Directory.GetFiles(RuntimeInfo.StartupDirectory, @$"{ruleset_library_prefix}.*.dll");
-
-                foreach (string file in files.Where(f => !Path.GetFileName(f).Contains("Tests")))
-                    loadRulesetFromFile(file);
-            }
-            catch (Exception e)
-            {
-                Logger.Error(e, $"Could not load rulesets from directory {RuntimeInfo.StartupDirectory}");
-            }
-        }
-
-        private void loadRulesetFromFile(string file)
-        {
-            string? filename = Path.GetFileNameWithoutExtension(file);
-
-            if (loadedAssemblies.Values.Any(t => Path.GetFileNameWithoutExtension(t.Assembly.Location) == filename))
-                return;
-
-            try
-            {
-                addRuleset(Assembly.LoadFrom(file));
-            }
-            catch (Exception e)
-            {
-                Logger.Error(e, $"Failed to load ruleset {filename}");
-            }
-        }
-
-        private void addRuleset(Assembly assembly)
-        {
-            if (loadedAssemblies.ContainsKey(assembly))
-                return;
-
-            // the same assembly may be loaded twice in the same AppDomain (currently a thing in certain Rider versions https://youtrack.jetbrains.com/issue/RIDER-48799).
-            // as a failsafe, also compare by FullName.
-            if (loadedAssemblies.Any(a => a.Key.FullName == assembly.FullName))
-                return;
-
-            try
-            {
-                loadedAssemblies[assembly] = assembly.GetTypes().First(t => t.IsPublic && t.IsSubclassOf(typeof(Ruleset)));
-            }
-            catch (Exception e)
-            {
-                Logger.Error(e, $"Failed to add ruleset {assembly}");
-            }
-        }
-
-        public void Dispose()
-        {
-            Dispose(true);
-            GC.SuppressFinalize(this);
-        }
-
-        protected virtual void Dispose(bool disposing)
-        {
-            AppDomain.CurrentDomain.AssemblyResolve -= resolveRulesetDependencyAssembly;
-        }
-
-        #region Implementation of IRulesetStore
-
-        IRulesetInfo? IRulesetStore.GetRuleset(int id) => GetRuleset(id);
-        IRulesetInfo? IRulesetStore.GetRuleset(string shortName) => GetRuleset(shortName);
-        IEnumerable<IRulesetInfo> IRulesetStore.AvailableRulesets => AvailableRulesets;
-
-        #endregion
-    }
-}
diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs
index 8a31e4576a..3d6240bc98 100644
--- a/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs
+++ b/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs
@@ -10,8 +10,9 @@ using osu.Framework.Graphics;
 using osu.Framework.Graphics.Containers;
 using osu.Framework.Graphics.Textures;
 using osu.Framework.Platform;
-using osu.Game.IO;
+using osu.Game.Database;
 using osu.Game.Screens.Play;
+using osu.Game.Stores;
 
 namespace osu.Game.Storyboards.Drawables
 {
@@ -76,12 +77,12 @@ namespace osu.Game.Storyboards.Drawables
         }
 
         [BackgroundDependencyLoader(true)]
-        private void load(FileStore fileStore, GameplayClock clock, CancellationToken? cancellationToken, GameHost host)
+        private void load(GameplayClock clock, CancellationToken? cancellationToken, GameHost host, RealmContextFactory realmContextFactory)
         {
             if (clock != null)
                 Clock = clock;
 
-            dependencies.Cache(new TextureStore(host.CreateTextureLoaderStore(fileStore.Store), false, scaleAdjust: 1));
+            dependencies.Cache(new TextureStore(host.CreateTextureLoaderStore(new RealmFileStore(realmContextFactory, host.Storage).Store), false, scaleAdjust: 1));
 
             foreach (var layer in Storyboard.Layers)
             {
diff --git a/osu.Game/Storyboards/Storyboard.cs b/osu.Game/Storyboards/Storyboard.cs
index fa90a00f0d..b86deeab89 100644
--- a/osu.Game/Storyboards/Storyboard.cs
+++ b/osu.Game/Storyboards/Storyboard.cs
@@ -78,7 +78,7 @@ namespace osu.Game.Storyboards
         {
             get
             {
-                string backgroundPath = BeatmapInfo.BeatmapSet?.Metadata?.BackgroundFile;
+                string backgroundPath = BeatmapInfo.BeatmapSet?.Metadata.BackgroundFile;
 
                 if (string.IsNullOrEmpty(backgroundPath))
                     return false;
@@ -96,7 +96,8 @@ namespace osu.Game.Storyboards
         public Drawable CreateSpriteFromResourcePath(string path, TextureStore textureStore)
         {
             Drawable drawable = null;
-            string storyboardPath = BeatmapInfo.BeatmapSet?.Files.Find(f => f.Filename.Equals(path, StringComparison.OrdinalIgnoreCase))?.FileInfo.GetStoragePath();
+
+            string storyboardPath = BeatmapInfo.BeatmapSet?.Files.FirstOrDefault(f => f.Filename.Equals(path, StringComparison.OrdinalIgnoreCase))?.File.GetStoragePath();
 
             if (!string.IsNullOrEmpty(storyboardPath))
                 drawable = new Sprite { Texture = textureStore.Get(storyboardPath) };
diff --git a/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs b/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs
index 27d1de83ec..10cb210f4d 100644
--- a/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs
+++ b/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs
@@ -3,6 +3,7 @@
 
 using System;
 using System.Collections.Generic;
+using System.Diagnostics;
 using System.IO;
 using System.Linq;
 using System.Threading;
@@ -78,7 +79,11 @@ namespace osu.Game.Tests.Beatmaps
                         currentTestBeatmap = Decoder.GetDecoder<Beatmap>(reader).Decode(reader);
 
                     // populate ruleset for beatmap converters that require it to be present.
-                    currentTestBeatmap.BeatmapInfo.Ruleset = rulesetStore.GetRuleset(currentTestBeatmap.BeatmapInfo.RulesetID);
+                    var ruleset = rulesetStore.GetRuleset(currentTestBeatmap.BeatmapInfo.RulesetID);
+
+                    Debug.Assert(ruleset != null);
+
+                    currentTestBeatmap.BeatmapInfo.Ruleset = ruleset;
                 });
             });
 
@@ -93,12 +98,10 @@ namespace osu.Game.Tests.Beatmaps
                 userSkinInfo.Files.Clear();
                 userSkinInfo.Files.Add(new RealmNamedFileUsage(new RealmFile { Hash = userFile }, userFile));
 
+                Debug.Assert(beatmapInfo.BeatmapSet != null);
+
                 beatmapInfo.BeatmapSet.Files.Clear();
-                beatmapInfo.BeatmapSet.Files.Add(new BeatmapSetFileInfo
-                {
-                    Filename = beatmapFile,
-                    FileInfo = new IO.FileInfo { Hash = beatmapFile }
-                });
+                beatmapInfo.BeatmapSet.Files.Add(new RealmNamedFileUsage(new RealmFile { Hash = beatmapFile }, beatmapFile));
 
                 // Need to refresh the cached skin source to refresh the skin resource store.
                 dependencies.SkinSource = new SkinProvidingContainer(Skin = new LegacySkin(userSkinInfo, this));
diff --git a/osu.Game/Tests/Beatmaps/TestBeatmap.cs b/osu.Game/Tests/Beatmaps/TestBeatmap.cs
index 99944bcf6d..3b4547cb49 100644
--- a/osu.Game/Tests/Beatmaps/TestBeatmap.cs
+++ b/osu.Game/Tests/Beatmaps/TestBeatmap.cs
@@ -32,14 +32,12 @@ namespace osu.Game.Tests.Beatmaps
                 HitObjects = baseBeatmap.HitObjects;
 
             BeatmapInfo.Ruleset = ruleset;
-            BeatmapInfo.RulesetID = ruleset.ID ?? 0;
             BeatmapInfo.Length = 75000;
             BeatmapInfo.OnlineInfo = new APIBeatmap();
             BeatmapInfo.OnlineID = Interlocked.Increment(ref onlineBeatmapID);
 
             Debug.Assert(BeatmapInfo.BeatmapSet != null);
 
-            BeatmapInfo.BeatmapSet.Metadata = BeatmapInfo.Metadata;
             BeatmapInfo.BeatmapSet.Beatmaps.Add(BeatmapInfo);
             BeatmapInfo.BeatmapSet.OnlineID = Interlocked.Increment(ref onlineSetID);
         }
diff --git a/osu.Game/Tests/Visual/EditorClockTestScene.cs b/osu.Game/Tests/Visual/EditorClockTestScene.cs
index c2e9892735..66ab427565 100644
--- a/osu.Game/Tests/Visual/EditorClockTestScene.cs
+++ b/osu.Game/Tests/Visual/EditorClockTestScene.cs
@@ -35,9 +35,9 @@ namespace osu.Game.Tests.Visual
             return dependencies;
         }
 
-        [BackgroundDependencyLoader]
-        private void load()
+        protected override void LoadComplete()
         {
+            base.LoadComplete();
             Beatmap.BindValueChanged(beatmapChanged, true);
         }
 
diff --git a/osu.Game/Tests/Visual/EditorTestScene.cs b/osu.Game/Tests/Visual/EditorTestScene.cs
index 07152b5a3e..6cc009514d 100644
--- a/osu.Game/Tests/Visual/EditorTestScene.cs
+++ b/osu.Game/Tests/Visual/EditorTestScene.cs
@@ -42,17 +42,27 @@ namespace osu.Game.Tests.Visual
             Alpha = 0
         };
 
+        private TestBeatmapManager testBeatmapManager;
+        private WorkingBeatmap working;
+
         [BackgroundDependencyLoader]
         private void load(GameHost host, AudioManager audio, RulesetStore rulesets)
         {
             Add(logo);
 
-            var working = CreateWorkingBeatmap(Ruleset.Value);
-
-            Beatmap.Value = working;
+            working = CreateWorkingBeatmap(Ruleset.Value);
 
             if (IsolateSavingFromDatabase)
-                Dependencies.CacheAs<BeatmapManager>(new TestBeatmapManager(LocalStorage, ContextFactory, rulesets, null, audio, Resources, host, Beatmap.Default, working));
+                Dependencies.CacheAs<BeatmapManager>(testBeatmapManager = new TestBeatmapManager(LocalStorage, ContextFactory, rulesets, null, audio, Resources, host, Beatmap.Default));
+        }
+
+        protected override void LoadComplete()
+        {
+            base.LoadComplete();
+
+            Beatmap.Value = working;
+            if (testBeatmapManager != null)
+                testBeatmapManager.TestBeatmap = working;
         }
 
         protected virtual bool EditorComponentsReady => Editor.ChildrenOfType<HitObjectComposer>().FirstOrDefault()?.IsLoaded == true
@@ -114,17 +124,16 @@ namespace osu.Game.Tests.Visual
 
         private class TestBeatmapManager : BeatmapManager
         {
-            private readonly WorkingBeatmap testBeatmap;
+            public WorkingBeatmap TestBeatmap;
 
-            public TestBeatmapManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, IAPIProvider api, [NotNull] AudioManager audioManager, IResourceStore<byte[]> resources, GameHost host, WorkingBeatmap defaultBeatmap, WorkingBeatmap testBeatmap)
+            public TestBeatmapManager(Storage storage, RealmContextFactory contextFactory, RulesetStore rulesets, IAPIProvider api, [NotNull] AudioManager audioManager, IResourceStore<byte[]> resources, GameHost host, WorkingBeatmap defaultBeatmap)
                 : base(storage, contextFactory, rulesets, api, audioManager, resources, host, defaultBeatmap)
             {
-                this.testBeatmap = testBeatmap;
             }
 
-            protected override BeatmapModelManager CreateBeatmapModelManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, IAPIProvider api, GameHost host)
+            protected override BeatmapModelManager CreateBeatmapModelManager(Storage storage, RealmContextFactory contextFactory, RulesetStore rulesets, BeatmapOnlineLookupQueue onlineLookupQueue)
             {
-                return new TestBeatmapModelManager(storage, contextFactory, rulesets, api, host);
+                return new TestBeatmapModelManager(storage, contextFactory, rulesets, onlineLookupQueue);
             }
 
             protected override WorkingBeatmapCache CreateWorkingBeatmapCache(AudioManager audioManager, IResourceStore<byte[]> resources, IResourceStore<byte[]> storage, WorkingBeatmap defaultBeatmap, GameHost host)
@@ -143,13 +152,13 @@ namespace osu.Game.Tests.Visual
                 }
 
                 public override WorkingBeatmap GetWorkingBeatmap(BeatmapInfo beatmapInfo)
-                    => testBeatmapManager.testBeatmap;
+                    => testBeatmapManager.TestBeatmap;
             }
 
             internal class TestBeatmapModelManager : BeatmapModelManager
             {
-                public TestBeatmapModelManager(Storage storage, IDatabaseContextFactory databaseContextFactory, RulesetStore rulesetStore, IAPIProvider apiProvider, GameHost gameHost)
-                    : base(storage, databaseContextFactory, rulesetStore, gameHost)
+                public TestBeatmapModelManager(Storage storage, RealmContextFactory databaseContextFactory, RulesetStore rulesetStore, BeatmapOnlineLookupQueue beatmapOnlineLookupQueue)
+                    : base(databaseContextFactory, storage, beatmapOnlineLookupQueue)
                 {
                 }
 
diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs
index 76acac3f8b..15ede6cc26 100644
--- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs
+++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs
@@ -409,18 +409,18 @@ namespace osu.Game.Tests.Visual.Multiplayer
 
         public override Task<APIBeatmap> GetAPIBeatmap(int beatmapId, CancellationToken cancellationToken = default)
         {
-            IBeatmapSetInfo? set = roomManager.ServerSideRooms.SelectMany(r => r.Playlist)
-                                              .FirstOrDefault(p => p.BeatmapID == beatmapId)?.Beatmap.Value.BeatmapSet
-                                   ?? beatmaps.QueryBeatmap(b => b.OnlineID == beatmapId)?.BeatmapSet;
+            IBeatmapInfo? beatmap = roomManager.ServerSideRooms.SelectMany(r => r.Playlist)
+                                               .FirstOrDefault(p => p.BeatmapID == beatmapId)?.Beatmap.Value
+                                    ?? beatmaps.QueryBeatmap(b => b.OnlineID == beatmapId);
 
-            if (set == null)
+            if (beatmap == null)
                 throw new InvalidOperationException("Beatmap not found.");
 
             return Task.FromResult(new APIBeatmap
             {
-                BeatmapSet = new APIBeatmapSet { OnlineID = set.OnlineID },
+                BeatmapSet = new APIBeatmapSet { OnlineID = beatmap.BeatmapSet?.OnlineID ?? -1 },
                 OnlineID = beatmapId,
-                Checksum = set.Beatmaps.First(b => b.OnlineID == beatmapId).MD5Hash
+                Checksum = beatmap.MD5Hash
             });
         }
 
diff --git a/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs b/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs
index 520f2c4585..5a0a7e71d4 100644
--- a/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs
+++ b/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs
@@ -70,6 +70,29 @@ namespace osu.Game.Tests.Visual.OnlinePlay
                     return true;
                 }
 
+                case GetRoomLeaderboardRequest roomLeaderboardRequest:
+                    roomLeaderboardRequest.TriggerSuccess(new APILeaderboard
+                    {
+                        Leaderboard = new List<APIUserScoreAggregate>
+                        {
+                            new APIUserScoreAggregate
+                            {
+                                TotalScore = 1000000,
+                                TotalAttempts = 5,
+                                CompletedBeatmaps = 2,
+                                User = new APIUser { Username = "best user" }
+                            },
+                            new APIUserScoreAggregate
+                            {
+                                TotalScore = 50,
+                                TotalAttempts = 1,
+                                CompletedBeatmaps = 1,
+                                User = new APIUser { Username = "worst user" }
+                            }
+                        }
+                    });
+                    return true;
+
                 case PartRoomRequest partRoomRequest:
                     partRoomRequest.TriggerSuccess();
                     return true;
diff --git a/osu.Game/Tests/Visual/OsuTestScene.cs b/osu.Game/Tests/Visual/OsuTestScene.cs
index 6b029729ea..da8af49158 100644
--- a/osu.Game/Tests/Visual/OsuTestScene.cs
+++ b/osu.Game/Tests/Visual/OsuTestScene.cs
@@ -28,7 +28,6 @@ using osu.Game.Rulesets;
 using osu.Game.Rulesets.Mods;
 using osu.Game.Rulesets.Objects;
 using osu.Game.Rulesets.UI;
-using osu.Game.Screens;
 using osu.Game.Storyboards;
 using osu.Game.Tests.Beatmaps;
 using osu.Game.Tests.Rulesets;
@@ -38,13 +37,16 @@ namespace osu.Game.Tests.Visual
     [ExcludeFromDynamicCompile]
     public abstract class OsuTestScene : TestScene
     {
-        protected Bindable<WorkingBeatmap> Beatmap { get; private set; }
+        [Cached]
+        protected Bindable<WorkingBeatmap> Beatmap { get; } = new Bindable<WorkingBeatmap>();
 
-        protected Bindable<RulesetInfo> Ruleset;
+        [Cached]
+        protected Bindable<RulesetInfo> Ruleset { get; } = new Bindable<RulesetInfo>();
 
-        protected Bindable<IReadOnlyList<Mod>> SelectedMods;
+        [Cached]
+        protected Bindable<IReadOnlyList<Mod>> SelectedMods { get; } = new Bindable<IReadOnlyList<Mod>>(Array.Empty<Mod>());
 
-        protected new OsuScreenDependencies Dependencies { get; private set; }
+        protected new DependencyContainer Dependencies { get; private set; }
 
         protected IResourceStore<byte[]> Resources;
 
@@ -73,9 +75,9 @@ namespace osu.Game.Tests.Visual
         /// <remarks>
         /// In interactive runs (ie. VisualTests) this will use the user's database if <see cref="UseFreshStoragePerRun"/> is not set to <c>true</c>.
         /// </remarks>
-        protected DatabaseContextFactory ContextFactory => contextFactory.Value;
+        protected RealmContextFactory ContextFactory => contextFactory.Value;
 
-        private Lazy<DatabaseContextFactory> contextFactory;
+        private Lazy<RealmContextFactory> contextFactory;
 
         /// <summary>
         /// Whether a fresh storage should be initialised per test (method) run.
@@ -117,14 +119,7 @@ namespace osu.Game.Tests.Visual
 
             Resources = parent.Get<OsuGameBase>().Resources;
 
-            contextFactory = new Lazy<DatabaseContextFactory>(() =>
-            {
-                var factory = new DatabaseContextFactory(LocalStorage);
-
-                using (var usage = factory.Get())
-                    usage.Migrate();
-                return factory;
-            });
+            contextFactory = new Lazy<RealmContextFactory>(() => new RealmContextFactory(LocalStorage, "client"));
 
             RecycleLocalStorage(false);
 
@@ -139,17 +134,15 @@ namespace osu.Game.Tests.Visual
 
             var providedRuleset = CreateRuleset();
             if (providedRuleset != null)
-                baseDependencies = rulesetDependencies = new DrawableRulesetDependencies(providedRuleset, baseDependencies);
+                isolatedBaseDependencies = rulesetDependencies = new DrawableRulesetDependencies(providedRuleset, baseDependencies);
 
-            Dependencies = new OsuScreenDependencies(false, baseDependencies);
+            Dependencies = isolatedBaseDependencies;
 
-            Beatmap = Dependencies.Beatmap;
+            Beatmap.Default = parent.Get<Bindable<WorkingBeatmap>>().Default;
             Beatmap.SetDefault();
 
-            Ruleset = Dependencies.Ruleset;
-            Ruleset.SetDefault();
+            Ruleset.Value = CreateRuleset()?.RulesetInfo ?? parent.Get<RulesetStore>().AvailableRulesets.First();
 
-            SelectedMods = Dependencies.Mods;
             SelectedMods.SetDefault();
 
             if (!UseOnlineAPI)
@@ -162,6 +155,23 @@ namespace osu.Game.Tests.Visual
             return Dependencies;
         }
 
+        protected override void LoadComplete()
+        {
+            base.LoadComplete();
+
+            var parentBeatmap = Parent.Dependencies.Get<Bindable<WorkingBeatmap>>();
+            parentBeatmap.Value = Beatmap.Value;
+            Beatmap.BindTo(parentBeatmap);
+
+            var parentRuleset = Parent.Dependencies.Get<Bindable<RulesetInfo>>();
+            parentRuleset.Value = Ruleset.Value;
+            Ruleset.BindTo(parentRuleset);
+
+            var parentMods = Parent.Dependencies.Get<Bindable<IReadOnlyList<Mod>>>();
+            parentMods.Value = SelectedMods.Value;
+            SelectedMods.BindTo(parentMods);
+        }
+
         protected override Container<Drawable> Content => content ?? base.Content;
 
         private readonly Container content;
@@ -286,12 +296,6 @@ namespace osu.Game.Tests.Visual
         protected virtual WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null) =>
             new ClockBackedTestWorkingBeatmap(beatmap, storyboard, Clock, Audio);
 
-        [BackgroundDependencyLoader]
-        private void load(RulesetStore rulesets)
-        {
-            Ruleset.Value = CreateRuleset()?.RulesetInfo ?? rulesets.AvailableRulesets.First();
-        }
-
         protected override void Dispose(bool isDisposing)
         {
             base.Dispose(isDisposing);
@@ -301,9 +305,6 @@ namespace osu.Game.Tests.Visual
             if (MusicController?.TrackLoaded == true)
                 MusicController.Stop();
 
-            if (contextFactory?.IsValueCreated == true)
-                contextFactory.Value.ResetDatabase();
-
             RecycleLocalStorage(true);
         }
 
diff --git a/osu.Game/Tests/Visual/RateAdjustedBeatmapTestScene.cs b/osu.Game/Tests/Visual/RateAdjustedBeatmapTestScene.cs
index ad24ffc7b8..1034f208a9 100644
--- a/osu.Game/Tests/Visual/RateAdjustedBeatmapTestScene.cs
+++ b/osu.Game/Tests/Visual/RateAdjustedBeatmapTestScene.cs
@@ -12,8 +12,11 @@ namespace osu.Game.Tests.Visual
         {
             base.Update();
 
-            // note that this will override any mod rate application
-            Beatmap.Value.Track.Tempo.Value = Clock.Rate;
+            if (Beatmap.Value.TrackLoaded)
+            {
+                // note that this will override any mod rate application
+                Beatmap.Value.Track.Tempo.Value = Clock.Rate;
+            }
         }
     }
 }
diff --git a/osu.Game/Tests/Visual/SkinnableTestScene.cs b/osu.Game/Tests/Visual/SkinnableTestScene.cs
index 22aac79056..a080f47d66 100644
--- a/osu.Game/Tests/Visual/SkinnableTestScene.cs
+++ b/osu.Game/Tests/Visual/SkinnableTestScene.cs
@@ -41,7 +41,7 @@ namespace osu.Game.Tests.Visual
         }
 
         [BackgroundDependencyLoader]
-        private void load(AudioManager audio, SkinManager skinManager)
+        private void load()
         {
             var dllStore = new DllResourceStore(DynamicCompilationOriginal.GetType().Assembly);
 
diff --git a/osu.Game/Tests/Visual/TestPlayer.cs b/osu.Game/Tests/Visual/TestPlayer.cs
index d68984b144..368f792e28 100644
--- a/osu.Game/Tests/Visual/TestPlayer.cs
+++ b/osu.Game/Tests/Visual/TestPlayer.cs
@@ -94,6 +94,9 @@ namespace osu.Game.Tests.Visual
         [BackgroundDependencyLoader]
         private void load()
         {
+            if (!LoadedBeatmapSuccessfully)
+                return;
+
             ScoreProcessor.NewJudgement += r => Results.Add(r);
         }
     }
diff --git a/osu.Game/Users/Drawables/ClickableAvatar.cs b/osu.Game/Users/Drawables/ClickableAvatar.cs
index 2825c41ef6..e3cfaf1d14 100644
--- a/osu.Game/Users/Drawables/ClickableAvatar.cs
+++ b/osu.Game/Users/Drawables/ClickableAvatar.cs
@@ -4,7 +4,6 @@
 using osu.Framework.Allocation;
 using osu.Framework.Graphics;
 using osu.Framework.Graphics.Containers;
-using osu.Framework.Graphics.Textures;
 using osu.Framework.Input.Events;
 using osu.Framework.Localisation;
 using osu.Game.Graphics.Containers;
@@ -58,7 +57,7 @@ namespace osu.Game.Users.Drawables
         }
 
         [BackgroundDependencyLoader]
-        private void load(LargeTextureStore textures)
+        private void load()
         {
             LoadComponentAsync(new DrawableAvatar(user), clickableArea.Add);
         }
diff --git a/osu.Game/Users/IUser.cs b/osu.Game/Users/IUser.cs
index 3995531fd9..d9a352872f 100644
--- a/osu.Game/Users/IUser.cs
+++ b/osu.Game/Users/IUser.cs
@@ -1,14 +1,25 @@
 // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
 // See the LICENCE file in the repository root for full licence text.
 
+using System;
 using osu.Game.Database;
 
+#nullable enable
+
 namespace osu.Game.Users
 {
-    public interface IUser : IHasOnlineID<int>
+    public interface IUser : IHasOnlineID<int>, IEquatable<IUser>
     {
         string Username { get; }
 
         bool IsBot { get; }
+
+        bool IEquatable<IUser>.Equals(IUser? other)
+        {
+            if (other == null)
+                return false;
+
+            return OnlineID == other.OnlineID && Username == other.Username;
+        }
     }
 }
diff --git a/osu.Game/Utils/SentryLogger.cs b/osu.Game/Utils/SentryLogger.cs
index 8f12760a6b..dbf04283b6 100644
--- a/osu.Game/Utils/SentryLogger.cs
+++ b/osu.Game/Utils/SentryLogger.cs
@@ -16,6 +16,7 @@ namespace osu.Game.Utils
     {
         private SentryClient sentry;
         private Scope sentryScope;
+        private Exception lastException;
 
         public SentryLogger(OsuGame game)
         {
@@ -30,30 +31,27 @@ namespace osu.Game.Utils
             sentry = new SentryClient(options);
             sentryScope = new Scope(options);
 
-            Exception lastException = null;
+            Logger.NewEntry += processLogEntry;
+        }
 
-            Logger.NewEntry += entry =>
+        private void processLogEntry(LogEntry entry)
+        {
+            if (entry.Level < LogLevel.Verbose) return;
+
+            var exception = entry.Exception;
+
+            if (exception != null)
             {
-                if (entry.Level < LogLevel.Verbose) return;
+                if (!shouldSubmitException(exception)) return;
 
-                var exception = entry.Exception;
+                // since we let unhandled exceptions go ignored at times, we want to ensure they don't get submitted on subsequent reports.
+                if (lastException != null && lastException.Message == exception.Message && exception.StackTrace.StartsWith(lastException.StackTrace, StringComparison.Ordinal)) return;
 
-                if (exception != null)
-                {
-                    if (!shouldSubmitException(exception))
-                        return;
-
-                    // since we let unhandled exceptions go ignored at times, we want to ensure they don't get submitted on subsequent reports.
-                    if (lastException != null &&
-                        lastException.Message == exception.Message && exception.StackTrace.StartsWith(lastException.StackTrace, StringComparison.Ordinal))
-                        return;
-
-                    lastException = exception;
-                    sentry.CaptureEvent(new SentryEvent(exception) { Message = entry.Message }, sentryScope);
-                }
-                else
-                    sentryScope.AddBreadcrumb(DateTimeOffset.Now, entry.Message, entry.Target.ToString(), "navigation");
-            };
+                lastException = exception;
+                sentry.CaptureEvent(new SentryEvent(exception) { Message = entry.Message }, sentryScope);
+            }
+            else
+                sentryScope.AddBreadcrumb(DateTimeOffset.Now, entry.Message, entry.Target.ToString(), "navigation");
         }
 
         private bool shouldSubmitException(Exception exception)
@@ -92,15 +90,9 @@ namespace osu.Game.Utils
             GC.SuppressFinalize(this);
         }
 
-        private bool isDisposed;
-
         protected virtual void Dispose(bool isDisposing)
         {
-            if (isDisposed)
-                return;
-
-            isDisposed = true;
-            sentry?.Dispose();
+            Logger.NewEntry -= processLogEntry;
             sentry = null;
             sentryScope = null;
         }
diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj
index 2609f17c73..758575e74a 100644
--- a/osu.Game/osu.Game.csproj
+++ b/osu.Game/osu.Game.csproj
@@ -18,9 +18,9 @@
     </None>
   </ItemGroup>
   <ItemGroup Label="Package References">
-    <PackageReference Include="AutoMapper" Version="10.1.1" />
+    <PackageReference Include="AutoMapper" Version="11.0.0" />
     <PackageReference Include="DiffPlex" Version="1.7.0" />
-    <PackageReference Include="HtmlAgilityPack" Version="1.11.39" />
+    <PackageReference Include="HtmlAgilityPack" Version="1.11.40" />
     <PackageReference Include="Humanizer" Version="2.13.14" />
     <PackageReference Include="MessagePack" Version="2.3.85" />
     <PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="5.0.11" />
@@ -35,10 +35,10 @@
       <PrivateAssets>all</PrivateAssets>
       <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
     </PackageReference>
-    <PackageReference Include="Realm" Version="10.7.1" />
-    <PackageReference Include="ppy.osu.Framework" Version="2022.111.0" />
-    <PackageReference Include="ppy.osu.Game.Resources" Version="2022.114.0" />
-    <PackageReference Include="Sentry" Version="3.12.1" />
+    <PackageReference Include="Realm" Version="10.8.0" />
+    <PackageReference Include="ppy.osu.Framework" Version="2022.118.0" />
+    <PackageReference Include="ppy.osu.Game.Resources" Version="2022.115.0" />
+    <PackageReference Include="Sentry" Version="3.13.0" />
     <PackageReference Include="SharpCompress" Version="0.30.1" />
     <PackageReference Include="NUnit" Version="3.13.2" />
     <PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
diff --git a/osu.iOS.props b/osu.iOS.props
index 0064a597fd..5925581e28 100644
--- a/osu.iOS.props
+++ b/osu.iOS.props
@@ -60,8 +60,8 @@
     <Reference Include="System.Net.Http" />
   </ItemGroup>
   <ItemGroup Label="Package References">
-    <PackageReference Include="ppy.osu.Framework.iOS" Version="2022.111.0" />
-    <PackageReference Include="ppy.osu.Game.Resources" Version="2022.114.0" />
+    <PackageReference Include="ppy.osu.Framework.iOS" Version="2022.118.0" />
+    <PackageReference Include="ppy.osu.Game.Resources" Version="2022.115.0" />
   </ItemGroup>
   <!-- See https://github.com/dotnet/runtime/issues/35988 (can be removed after Xamarin uses net5.0 / net6.0) -->
   <PropertyGroup>
@@ -83,11 +83,11 @@
     <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
     <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
     <PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
-    <PackageReference Include="ppy.osu.Framework" Version="2022.111.0" />
+    <PackageReference Include="ppy.osu.Framework" Version="2022.118.0" />
     <PackageReference Include="SharpCompress" Version="0.30.0" />
     <PackageReference Include="NUnit" Version="3.13.2" />
     <PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
     <PackageReference Include="ppy.osu.Framework.NativeLibs" Version="2021.805.0" ExcludeAssets="all" />
-    <PackageReference Include="Realm" Version="10.7.1" />
+    <PackageReference Include="Realm" Version="10.8.0" />
   </ItemGroup>
 </Project>