From 9542e3e24ada33a0d63030584b7c109f940a1ab3 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Tue, 25 Dec 2018 17:59:28 +0900
Subject: [PATCH 1/9] Add Updateable beatmap sprite

---
 osu.Game/Beatmaps/BeatmapManager.cs           |  2 +-
 .../UpdateableBeatmapBackgroundSprite.cs      | 47 +++++++++++++++++++
 2 files changed, 48 insertions(+), 1 deletion(-)
 create mode 100644 osu.Game/Beatmaps/Drawables/UpdateableBeatmapBackgroundSprite.cs

diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs
index c179821a7c..58c0819075 100644
--- a/osu.Game/Beatmaps/BeatmapManager.cs
+++ b/osu.Game/Beatmaps/BeatmapManager.cs
@@ -59,7 +59,7 @@ namespace osu.Game.Beatmaps
         /// <summary>
         /// A default representation of a WorkingBeatmap to use when no beatmap is available.
         /// </summary>
-        public WorkingBeatmap DefaultBeatmap { private get; set; }
+        public WorkingBeatmap DefaultBeatmap { get; set; }
 
         public override string[] HandledExtensions => new[] { ".osz" };
 
diff --git a/osu.Game/Beatmaps/Drawables/UpdateableBeatmapBackgroundSprite.cs b/osu.Game/Beatmaps/Drawables/UpdateableBeatmapBackgroundSprite.cs
new file mode 100644
index 0000000000..1808325466
--- /dev/null
+++ b/osu.Game/Beatmaps/Drawables/UpdateableBeatmapBackgroundSprite.cs
@@ -0,0 +1,47 @@
+// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
+// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
+
+using osu.Framework.Allocation;
+using osu.Framework.Configuration;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+
+namespace osu.Game.Beatmaps.Drawables
+{
+    /// <summary>
+    /// Display a baetmap background from a local source, but fallback to online source if not available.
+    /// </summary>
+    public class UpdateableBeatmapBackgroundSprite : ModelBackedDrawable<BeatmapInfo>
+    {
+        public readonly IBindable<BeatmapInfo> Beatmap = new Bindable<BeatmapInfo>();
+
+        [Resolved]
+        private BeatmapManager beatmaps { get; set; }
+
+        public UpdateableBeatmapBackgroundSprite()
+        {
+            Beatmap.BindValueChanged(b => Schedule(() => Model = b));
+        }
+
+        protected override Drawable CreateDrawable(BeatmapInfo model)
+        {
+            Drawable drawable;
+
+            var localBeatmap = beatmaps.GetWorkingBeatmap(model);
+
+            if (localBeatmap == beatmaps.DefaultBeatmap && model?.BeatmapSet?.OnlineInfo != null)
+                drawable = new BeatmapSetCover(model.BeatmapSet);
+            else
+                drawable = new BeatmapBackgroundSprite(localBeatmap);
+
+            drawable.RelativeSizeAxes = Axes.Both;
+            drawable.Anchor = Anchor.Centre;
+            drawable.Origin = Anchor.Centre;
+            drawable.FillMode = FillMode.Fill;
+
+            return drawable;
+        }
+
+        protected override double FadeDuration => 400;
+    }
+}

From 96c9e5f209bf6e9b8077997cd902a1a3987eb18d Mon Sep 17 00:00:00 2001
From: smoogipoo <smoogipoo@smgi.me>
Date: Tue, 25 Dec 2018 18:34:45 +0900
Subject: [PATCH 2/9] Make DefaultBeatmap readonly

---
 osu.Game.Tests/Visual/TestCasePlaySongSelect.cs |  5 +----
 osu.Game/Beatmaps/BeatmapManager.cs             | 15 +++++++++------
 osu.Game/OsuGameBase.cs                         |  9 ++++-----
 3 files changed, 14 insertions(+), 15 deletions(-)

diff --git a/osu.Game.Tests/Visual/TestCasePlaySongSelect.cs b/osu.Game.Tests/Visual/TestCasePlaySongSelect.cs
index d87a8d0056..29060ceb12 100644
--- a/osu.Game.Tests/Visual/TestCasePlaySongSelect.cs
+++ b/osu.Game.Tests/Visual/TestCasePlaySongSelect.cs
@@ -87,10 +87,7 @@ namespace osu.Game.Tests.Visual
                 usage.Migrate();
 
             Dependencies.Cache(rulesets = new RulesetStore(factory));
-            Dependencies.Cache(manager = new BeatmapManager(LocalStorage, factory, rulesets, null, null)
-            {
-                DefaultBeatmap = defaultBeatmap = Beatmap.Default
-            });
+            Dependencies.Cache(manager = new BeatmapManager(LocalStorage, factory, rulesets, null, null, null, defaultBeatmap = Beatmap.Default));
 
             Beatmap.SetDefault();
         }
diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs
index 58c0819075..73fd5da22c 100644
--- a/osu.Game/Beatmaps/BeatmapManager.cs
+++ b/osu.Game/Beatmaps/BeatmapManager.cs
@@ -59,7 +59,7 @@ namespace osu.Game.Beatmaps
         /// <summary>
         /// A default representation of a WorkingBeatmap to use when no beatmap is available.
         /// </summary>
-        public WorkingBeatmap DefaultBeatmap { get; set; }
+        public readonly WorkingBeatmap DefaultBeatmap;
 
         public override string[] HandledExtensions => new[] { ".osz" };
 
@@ -77,16 +77,19 @@ namespace osu.Game.Beatmaps
 
         private readonly List<DownloadBeatmapSetRequest> currentDownloads = new List<DownloadBeatmapSetRequest>();
 
-        public BeatmapManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, APIAccess api, AudioManager audioManager, IIpcHost importHost = null)
+        public BeatmapManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, APIAccess api, AudioManager audioManager, IIpcHost importHost = null,
+                              WorkingBeatmap defaultBeatmap = null)
             : base(storage, contextFactory, new BeatmapStore(contextFactory), importHost)
         {
-            beatmaps = (BeatmapStore)ModelStore;
-            beatmaps.BeatmapHidden += b => BeatmapHidden?.Invoke(b);
-            beatmaps.BeatmapRestored += b => BeatmapRestored?.Invoke(b);
-
             this.rulesets = rulesets;
             this.api = api;
             this.audioManager = audioManager;
+
+            DefaultBeatmap = defaultBeatmap;
+
+            beatmaps = (BeatmapStore)ModelStore;
+            beatmaps.BeatmapHidden += b => BeatmapHidden?.Invoke(b);
+            beatmaps.BeatmapRestored += b => BeatmapRestored?.Invoke(b);
         }
 
         protected override void Populate(BeatmapSetInfo beatmapSet, ArchiveReader archive)
diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs
index 0f1819d55b..002ef6ec32 100644
--- a/osu.Game/OsuGameBase.cs
+++ b/osu.Game/OsuGameBase.cs
@@ -153,9 +153,12 @@ namespace osu.Game
             dependencies.Cache(API);
             dependencies.CacheAs<IAPIProvider>(API);
 
+            var defaultBeatmap = new DummyWorkingBeatmap(this);
+            beatmap = new OsuBindableBeatmap(defaultBeatmap, Audio);
+
             dependencies.Cache(RulesetStore = new RulesetStore(contextFactory));
             dependencies.Cache(FileStore = new FileStore(contextFactory, Host.Storage));
-            dependencies.Cache(BeatmapManager = new BeatmapManager(Host.Storage, contextFactory, RulesetStore, API, Audio, Host));
+            dependencies.Cache(BeatmapManager = new BeatmapManager(Host.Storage, contextFactory, RulesetStore, API, Audio, Host, defaultBeatmap));
             dependencies.Cache(ScoreManager = new ScoreManager(RulesetStore, BeatmapManager, Host.Storage, contextFactory, Host));
             dependencies.Cache(KeyBindingStore = new KeyBindingStore(contextFactory, RulesetStore));
             dependencies.Cache(SettingsStore = new SettingsStore(contextFactory));
@@ -166,10 +169,6 @@ namespace osu.Game
             fileImporters.Add(ScoreManager);
             fileImporters.Add(SkinManager);
 
-            var defaultBeatmap = new DummyWorkingBeatmap(this);
-            beatmap = new OsuBindableBeatmap(defaultBeatmap, Audio);
-            BeatmapManager.DefaultBeatmap = defaultBeatmap;
-
             // tracks play so loud our samples can't keep up.
             // this adds a global reduction of track volume for the time being.
             Audio.Track.AddAdjustment(AdjustableProperty.Volume, new BindableDouble(0.8));

From c45c34d400003910f02638d359889222cc7fc3a3 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Tue, 25 Dec 2018 19:17:21 +0900
Subject: [PATCH 3/9] Make beatmap importing possible elsewhere in tests

---
 .../Beatmaps/IO/ImportBeatmapTest.cs          | 26 +++++++++----------
 1 file changed, 13 insertions(+), 13 deletions(-)

diff --git a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs
index 26167cb24a..c8fd531fcc 100644
--- a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs
+++ b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs
@@ -29,7 +29,7 @@ namespace osu.Game.Tests.Beatmaps.IO
             {
                 try
                 {
-                    loadOszIntoOsu(loadOsu(host));
+                    LoadOszIntoOsu(loadOsu(host));
                 }
                 finally
                 {
@@ -48,7 +48,7 @@ namespace osu.Game.Tests.Beatmaps.IO
                 {
                     var osu = loadOsu(host);
 
-                    var imported = loadOszIntoOsu(osu);
+                    var imported = LoadOszIntoOsu(osu);
 
                     deleteBeatmapSet(imported, osu);
                 }
@@ -69,8 +69,8 @@ namespace osu.Game.Tests.Beatmaps.IO
                 {
                     var osu = loadOsu(host);
 
-                    var imported = loadOszIntoOsu(osu);
-                    var importedSecondTime = loadOszIntoOsu(osu);
+                    var imported = LoadOszIntoOsu(osu);
+                    var importedSecondTime = LoadOszIntoOsu(osu);
 
                     // check the newly "imported" beatmap is actually just the restored previous import. since it matches hash.
                     Assert.IsTrue(imported.ID == importedSecondTime.ID);
@@ -105,7 +105,7 @@ namespace osu.Game.Tests.Beatmaps.IO
                     manager.ItemAdded += (_, __, ___) => fireCount++;
                     manager.ItemRemoved += _ => fireCount++;
 
-                    var imported = loadOszIntoOsu(osu);
+                    var imported = LoadOszIntoOsu(osu);
 
                     Assert.AreEqual(0, fireCount -= 1);
 
@@ -160,12 +160,12 @@ namespace osu.Game.Tests.Beatmaps.IO
                     var osu = loadOsu(host);
                     var manager = osu.Dependencies.Get<BeatmapManager>();
 
-                    var imported = loadOszIntoOsu(osu);
+                    var imported = LoadOszIntoOsu(osu);
 
                     imported.Hash += "-changed";
                     manager.Update(imported);
 
-                    var importedSecondTime = loadOszIntoOsu(osu);
+                    var importedSecondTime = LoadOszIntoOsu(osu);
 
                     Assert.IsTrue(imported.ID != importedSecondTime.ID);
                     Assert.IsTrue(imported.Beatmaps.First().ID < importedSecondTime.Beatmaps.First().ID);
@@ -191,11 +191,11 @@ namespace osu.Game.Tests.Beatmaps.IO
                 {
                     var osu = loadOsu(host);
 
-                    var imported = loadOszIntoOsu(osu);
+                    var imported = LoadOszIntoOsu(osu);
 
                     deleteBeatmapSet(imported, osu);
 
-                    var importedSecondTime = loadOszIntoOsu(osu);
+                    var importedSecondTime = LoadOszIntoOsu(osu);
 
                     // check the newly "imported" beatmap is actually just the restored previous import. since it matches hash.
                     Assert.IsTrue(imported.ID == importedSecondTime.ID);
@@ -262,7 +262,7 @@ namespace osu.Game.Tests.Beatmaps.IO
             }
         }
 
-        private string createTemporaryBeatmap()
+        private static string createTemporaryBeatmap()
         {
             var temp = Path.GetTempFileName() + ".osz";
             File.Copy(TEST_OSZ_PATH, temp, true);
@@ -270,7 +270,7 @@ namespace osu.Game.Tests.Beatmaps.IO
             return temp;
         }
 
-        private BeatmapSetInfo loadOszIntoOsu(OsuGameBase osu, string path = null)
+        public static BeatmapSetInfo LoadOszIntoOsu(OsuGameBase osu, string path = null)
         {
             var temp = path ?? createTemporaryBeatmap();
 
@@ -305,7 +305,7 @@ namespace osu.Game.Tests.Beatmaps.IO
             return osu;
         }
 
-        private void ensureLoaded(OsuGameBase osu, int timeout = 60000)
+        private static void ensureLoaded(OsuGameBase osu, int timeout = 60000)
         {
             IEnumerable<BeatmapSetInfo> resultSets = null;
             var store = osu.Dependencies.Get<BeatmapManager>();
@@ -343,7 +343,7 @@ namespace osu.Game.Tests.Beatmaps.IO
             Assert.IsTrue(beatmap?.HitObjects.Any() == true);
         }
 
-        private void waitForOrAssert(Func<bool> result, string failureMessage, int timeout = 60000)
+        private static void waitForOrAssert(Func<bool> result, string failureMessage, int timeout = 60000)
         {
             Task task = Task.Run(() =>
             {

From c902c1587a8248d78ba6d6a13af78fc9674f4e9e Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Tue, 25 Dec 2018 19:17:32 +0900
Subject: [PATCH 4/9] Add tests and modify fallback logic

---
 ...stCaseUpdateableBeatmapBackgroundSprite.cs | 51 +++++++++++++++++++
 .../UpdateableBeatmapBackgroundSprite.cs      |  2 +-
 2 files changed, 52 insertions(+), 1 deletion(-)
 create mode 100644 osu.Game.Tests/Visual/TestCaseUpdateableBeatmapBackgroundSprite.cs

diff --git a/osu.Game.Tests/Visual/TestCaseUpdateableBeatmapBackgroundSprite.cs b/osu.Game.Tests/Visual/TestCaseUpdateableBeatmapBackgroundSprite.cs
new file mode 100644
index 0000000000..24701fccbc
--- /dev/null
+++ b/osu.Game.Tests/Visual/TestCaseUpdateableBeatmapBackgroundSprite.cs
@@ -0,0 +1,51 @@
+// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
+// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
+
+using System.Linq;
+using osu.Framework.Allocation;
+using osu.Framework.Configuration;
+using osu.Framework.Graphics;
+using osu.Game.Beatmaps;
+using osu.Game.Beatmaps.Drawables;
+using osu.Game.Online.API;
+using osu.Game.Online.API.Requests;
+using osu.Game.Rulesets;
+using osu.Game.Tests.Beatmaps.IO;
+
+namespace osu.Game.Tests.Visual
+{
+    public class TestCaseUpdateableBeatmapBackgroundSprite : OsuTestCase
+    {
+        private UpdateableBeatmapBackgroundSprite backgroundSprite;
+
+        [Resolved]
+        private BeatmapManager beatmaps { get; set; }
+
+        [BackgroundDependencyLoader]
+        private void load(OsuGameBase osu, APIAccess api, RulesetStore rulesets)
+        {
+            Bindable<BeatmapInfo> beatmapBindable = new Bindable<BeatmapInfo>();
+
+            var imported = ImportBeatmapTest.LoadOszIntoOsu(osu);
+
+            Child = backgroundSprite = new UpdateableBeatmapBackgroundSprite { RelativeSizeAxes = Axes.Both };
+
+            backgroundSprite.Beatmap.BindTo(beatmapBindable);
+
+            var req = new GetBeatmapSetRequest(1);
+            api.Queue(req);
+
+            AddStep("null", () => beatmapBindable.Value = null);
+
+            AddStep("imported", () => beatmapBindable.Value = imported.Beatmaps.First());
+
+            if (api.IsLoggedIn)
+                AddUntilStep(() => req.Result != null, "wait for api response");
+
+            AddStep("online", () => beatmapBindable.Value = new BeatmapInfo
+            {
+                BeatmapSet = req.Result?.ToBeatmapSet(rulesets)
+            });
+        }
+    }
+}
diff --git a/osu.Game/Beatmaps/Drawables/UpdateableBeatmapBackgroundSprite.cs b/osu.Game/Beatmaps/Drawables/UpdateableBeatmapBackgroundSprite.cs
index 1808325466..fd00576d21 100644
--- a/osu.Game/Beatmaps/Drawables/UpdateableBeatmapBackgroundSprite.cs
+++ b/osu.Game/Beatmaps/Drawables/UpdateableBeatmapBackgroundSprite.cs
@@ -29,7 +29,7 @@ namespace osu.Game.Beatmaps.Drawables
 
             var localBeatmap = beatmaps.GetWorkingBeatmap(model);
 
-            if (localBeatmap == beatmaps.DefaultBeatmap && model?.BeatmapSet?.OnlineInfo != null)
+            if (localBeatmap.BeatmapInfo.ID == 0 && model?.BeatmapSet?.OnlineInfo != null)
                 drawable = new BeatmapSetCover(model.BeatmapSet);
             else
                 drawable = new BeatmapBackgroundSprite(localBeatmap);

From 3370de3c2eedfb1a38a89285459dcfd8fdb432a8 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Wed, 26 Dec 2018 11:53:24 +0900
Subject: [PATCH 5/9] Add rider live templates

---
 osu.sln.DotSettings | 121 ++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 121 insertions(+)

diff --git a/osu.sln.DotSettings b/osu.sln.DotSettings
index 112efb37f0..6b8e9dc808 100644
--- a/osu.sln.DotSettings
+++ b/osu.sln.DotSettings
@@ -673,6 +673,127 @@ Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/maste
 	<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002ECSharpPlaceAttributeOnSameLineMigration/@EntryIndexedValue">True</s:Boolean>
 	<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EMigrateBlankLinesAroundFieldToBlankLinesAroundProperty/@EntryIndexedValue">True</s:Boolean>
 	<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EMigrateThisQualifierSettings/@EntryIndexedValue">True</s:Boolean>
+	<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=28A2A5FC43E07C488A4BC7430879479E/@KeyIndexDefined">True</s:Boolean>
+	<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=28A2A5FC43E07C488A4BC7430879479E/Applicability/=Live/@EntryIndexedValue">True</s:Boolean>
+	<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=28A2A5FC43E07C488A4BC7430879479E/Description/@EntryValue">o!f – Object Initializer: Anchor&amp;Origin</s:String>
+	<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=28A2A5FC43E07C488A4BC7430879479E/Field/=anchor/@KeyIndexDefined">True</s:Boolean>
+	<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=28A2A5FC43E07C488A4BC7430879479E/Field/=anchor/Expression/@EntryValue">constant("Centre")</s:String>
+	<s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=28A2A5FC43E07C488A4BC7430879479E/Field/=anchor/Order/@EntryValue">0</s:Int64>
+	<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=28A2A5FC43E07C488A4BC7430879479E/Reformat/@EntryValue">True</s:Boolean>
+	<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=28A2A5FC43E07C488A4BC7430879479E/Scope/=C3001E7C0DA78E4487072B7E050D86C5/@KeyIndexDefined">True</s:Boolean>
+	<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=28A2A5FC43E07C488A4BC7430879479E/Scope/=C3001E7C0DA78E4487072B7E050D86C5/CustomProperties/=minimumLanguageVersion/@EntryIndexedValue">2.0</s:String>
+	<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=28A2A5FC43E07C488A4BC7430879479E/Scope/=C3001E7C0DA78E4487072B7E050D86C5/Type/@EntryValue">InCSharpFile</s:String>
+	<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=28A2A5FC43E07C488A4BC7430879479E/Shortcut/@EntryValue">ofao</s:String>
+	<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=28A2A5FC43E07C488A4BC7430879479E/ShortenQualifiedReferences/@EntryValue">True</s:Boolean>
+	<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=28A2A5FC43E07C488A4BC7430879479E/Text/@EntryValue">Anchor = Anchor.$anchor$,
+Origin = Anchor.$anchor$,</s:String>
+	<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=2A3ECBA387AF6D468F6ABDA35DED325A/@KeyIndexDefined">True</s:Boolean>
+	<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=2A3ECBA387AF6D468F6ABDA35DED325A/Applicability/=Live/@EntryIndexedValue">True</s:Boolean>
+	<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=2A3ECBA387AF6D468F6ABDA35DED325A/Description/@EntryValue">o!f – InternalChildren = []</s:String>
+	<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=2A3ECBA387AF6D468F6ABDA35DED325A/Reformat/@EntryValue">True</s:Boolean>
+	<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=2A3ECBA387AF6D468F6ABDA35DED325A/Scope/=C3001E7C0DA78E4487072B7E050D86C5/@KeyIndexDefined">True</s:Boolean>
+	<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=2A3ECBA387AF6D468F6ABDA35DED325A/Scope/=C3001E7C0DA78E4487072B7E050D86C5/CustomProperties/=minimumLanguageVersion/@EntryIndexedValue">2.0</s:String>
+	<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=2A3ECBA387AF6D468F6ABDA35DED325A/Scope/=C3001E7C0DA78E4487072B7E050D86C5/Type/@EntryValue">InCSharpFile</s:String>
+	<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=2A3ECBA387AF6D468F6ABDA35DED325A/Shortcut/@EntryValue">ofic</s:String>
+	<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=2A3ECBA387AF6D468F6ABDA35DED325A/ShortenQualifiedReferences/@EntryValue">True</s:Boolean>
+	<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=2A3ECBA387AF6D468F6ABDA35DED325A/Text/@EntryValue">InternalChildren = new Drawable[]
+{
+    $END$
+};</s:String>
+	<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=62B70E4DCA5E284A9E383E16C13789C1/@KeyIndexDefined">True</s:Boolean>
+	<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=62B70E4DCA5E284A9E383E16C13789C1/Applicability/=Live/@EntryIndexedValue">True</s:Boolean>
+	<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=62B70E4DCA5E284A9E383E16C13789C1/Description/@EntryValue">o!f – new GridContainer { .. }</s:String>
+	<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=62B70E4DCA5E284A9E383E16C13789C1/Reformat/@EntryValue">True</s:Boolean>
+	<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=62B70E4DCA5E284A9E383E16C13789C1/Scope/=C3001E7C0DA78E4487072B7E050D86C5/@KeyIndexDefined">True</s:Boolean>
+	<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=62B70E4DCA5E284A9E383E16C13789C1/Scope/=C3001E7C0DA78E4487072B7E050D86C5/CustomProperties/=minimumLanguageVersion/@EntryIndexedValue">2.0</s:String>
+	<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=62B70E4DCA5E284A9E383E16C13789C1/Scope/=C3001E7C0DA78E4487072B7E050D86C5/Type/@EntryValue">InCSharpFile</s:String>
+	<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=62B70E4DCA5E284A9E383E16C13789C1/Shortcut/@EntryValue">ofgc</s:String>
+	<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=62B70E4DCA5E284A9E383E16C13789C1/ShortenQualifiedReferences/@EntryValue">True</s:Boolean>
+	<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=62B70E4DCA5E284A9E383E16C13789C1/Text/@EntryValue">new GridContainer
+{
+    RelativeSizeAxes = Axes.Both,
+    Content = new[]
+    {
+        new Drawable[] { $END$ },
+        new Drawable[] { }
+    }
+};</s:String>
+	<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=72BD3C3DCA42C84DA1E71F1D05A903C4/@KeyIndexDefined">True</s:Boolean>
+	<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=72BD3C3DCA42C84DA1E71F1D05A903C4/Applicability/=Live/@EntryIndexedValue">True</s:Boolean>
+	<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=72BD3C3DCA42C84DA1E71F1D05A903C4/Description/@EntryValue">o!f – new FillFlowContainer { .. }</s:String>
+	<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=72BD3C3DCA42C84DA1E71F1D05A903C4/Reformat/@EntryValue">True</s:Boolean>
+	<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=72BD3C3DCA42C84DA1E71F1D05A903C4/Scope/=C3001E7C0DA78E4487072B7E050D86C5/@KeyIndexDefined">True</s:Boolean>
+	<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=72BD3C3DCA42C84DA1E71F1D05A903C4/Scope/=C3001E7C0DA78E4487072B7E050D86C5/CustomProperties/=minimumLanguageVersion/@EntryIndexedValue">2.0</s:String>
+	<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=72BD3C3DCA42C84DA1E71F1D05A903C4/Scope/=C3001E7C0DA78E4487072B7E050D86C5/Type/@EntryValue">InCSharpFile</s:String>
+	<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=72BD3C3DCA42C84DA1E71F1D05A903C4/Shortcut/@EntryValue">offf</s:String>
+	<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=72BD3C3DCA42C84DA1E71F1D05A903C4/ShortenQualifiedReferences/@EntryValue">True</s:Boolean>
+	<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=72BD3C3DCA42C84DA1E71F1D05A903C4/Text/@EntryValue">new FillFlowContainer
+{
+    RelativeSizeAxes = Axes.Both,
+    Direction = FillDirection.Vertical,
+    Children = new Drawable[]
+    {
+        $END$
+    }
+},</s:String>
+	<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=750A3C67E083484FAEEA0ED2382181CC/@KeyIndexDefined">True</s:Boolean>
+	<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=750A3C67E083484FAEEA0ED2382181CC/Applicability/=Live/@EntryIndexedValue">True</s:Boolean>
+	<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=750A3C67E083484FAEEA0ED2382181CC/Description/@EntryValue">o!f – new Container { .. }</s:String>
+	<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=750A3C67E083484FAEEA0ED2382181CC/Reformat/@EntryValue">True</s:Boolean>
+	<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=750A3C67E083484FAEEA0ED2382181CC/Scope/=C3001E7C0DA78E4487072B7E050D86C5/@KeyIndexDefined">True</s:Boolean>
+	<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=750A3C67E083484FAEEA0ED2382181CC/Scope/=C3001E7C0DA78E4487072B7E050D86C5/CustomProperties/=minimumLanguageVersion/@EntryIndexedValue">2.0</s:String>
+	<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=750A3C67E083484FAEEA0ED2382181CC/Scope/=C3001E7C0DA78E4487072B7E050D86C5/Type/@EntryValue">InCSharpFile</s:String>
+	<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=750A3C67E083484FAEEA0ED2382181CC/Shortcut/@EntryValue">ofcont</s:String>
+	<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=750A3C67E083484FAEEA0ED2382181CC/ShortenQualifiedReferences/@EntryValue">True</s:Boolean>
+	<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=750A3C67E083484FAEEA0ED2382181CC/Text/@EntryValue">new Container
+{
+    RelativeSizeAxes = Axes.Both,
+    Children = new Drawable[]
+    {
+        $END$
+    }
+},</s:String>
+	<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=78D9C2B1742FD449BD69CD18437E0C07/@KeyIndexDefined">True</s:Boolean>
+	<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=78D9C2B1742FD449BD69CD18437E0C07/Applicability/=Live/@EntryIndexedValue">True</s:Boolean>
+	<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=78D9C2B1742FD449BD69CD18437E0C07/Description/@EntryValue">o!f – BackgroundDependencyLoader load()</s:String>
+	<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=78D9C2B1742FD449BD69CD18437E0C07/Reformat/@EntryValue">True</s:Boolean>
+	<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=78D9C2B1742FD449BD69CD18437E0C07/Scope/=C3001E7C0DA78E4487072B7E050D86C5/@KeyIndexDefined">True</s:Boolean>
+	<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=78D9C2B1742FD449BD69CD18437E0C07/Scope/=C3001E7C0DA78E4487072B7E050D86C5/CustomProperties/=minimumLanguageVersion/@EntryIndexedValue">2.0</s:String>
+	<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=78D9C2B1742FD449BD69CD18437E0C07/Scope/=C3001E7C0DA78E4487072B7E050D86C5/Type/@EntryValue">InCSharpFile</s:String>
+	<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=78D9C2B1742FD449BD69CD18437E0C07/Shortcut/@EntryValue">ofbdl</s:String>
+	<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=78D9C2B1742FD449BD69CD18437E0C07/ShortenQualifiedReferences/@EntryValue">True</s:Boolean>
+	<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=78D9C2B1742FD449BD69CD18437E0C07/Text/@EntryValue">[BackgroundDependencyLoader]
+private void load()
+{
+    $END$
+}</s:String>
+	<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=CC879477D8841A4CBD724C2DCD249435/@KeyIndexDefined">True</s:Boolean>
+	<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=CC879477D8841A4CBD724C2DCD249435/Applicability/=Live/@EntryIndexedValue">True</s:Boolean>
+	<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=CC879477D8841A4CBD724C2DCD249435/Description/@EntryValue">o!f – new Box { .. }</s:String>
+	<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=CC879477D8841A4CBD724C2DCD249435/Reformat/@EntryValue">True</s:Boolean>
+	<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=CC879477D8841A4CBD724C2DCD249435/Scope/=C3001E7C0DA78E4487072B7E050D86C5/@KeyIndexDefined">True</s:Boolean>
+	<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=CC879477D8841A4CBD724C2DCD249435/Scope/=C3001E7C0DA78E4487072B7E050D86C5/CustomProperties/=minimumLanguageVersion/@EntryIndexedValue">2.0</s:String>
+	<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=CC879477D8841A4CBD724C2DCD249435/Scope/=C3001E7C0DA78E4487072B7E050D86C5/Type/@EntryValue">InCSharpFile</s:String>
+	<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=CC879477D8841A4CBD724C2DCD249435/Shortcut/@EntryValue">ofbox</s:String>
+	<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=CC879477D8841A4CBD724C2DCD249435/ShortenQualifiedReferences/@EntryValue">True</s:Boolean>
+	<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=CC879477D8841A4CBD724C2DCD249435/Text/@EntryValue">new Box
+{
+    Colour = Color4.Black,
+    RelativeSizeAxes = Axes.Both,
+},</s:String>
+	<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=F5B3CB743153774F99FB9FCA0FC744EE/@KeyIndexDefined">True</s:Boolean>
+	<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=F5B3CB743153774F99FB9FCA0FC744EE/Applicability/=Live/@EntryIndexedValue">True</s:Boolean>
+	<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=F5B3CB743153774F99FB9FCA0FC744EE/Description/@EntryValue">o!f – Children = []</s:String>
+	<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=F5B3CB743153774F99FB9FCA0FC744EE/Reformat/@EntryValue">True</s:Boolean>
+	<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=F5B3CB743153774F99FB9FCA0FC744EE/Scope/=C3001E7C0DA78E4487072B7E050D86C5/@KeyIndexDefined">True</s:Boolean>
+	<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=F5B3CB743153774F99FB9FCA0FC744EE/Scope/=C3001E7C0DA78E4487072B7E050D86C5/CustomProperties/=minimumLanguageVersion/@EntryIndexedValue">2.0</s:String>
+	<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=F5B3CB743153774F99FB9FCA0FC744EE/Scope/=C3001E7C0DA78E4487072B7E050D86C5/Type/@EntryValue">InCSharpFile</s:String>
+	<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=F5B3CB743153774F99FB9FCA0FC744EE/Shortcut/@EntryValue">ofc</s:String>
+	<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=F5B3CB743153774F99FB9FCA0FC744EE/ShortenQualifiedReferences/@EntryValue">True</s:Boolean>
+	<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=F5B3CB743153774F99FB9FCA0FC744EE/Text/@EntryValue">Children = new Drawable[]
+{
+    $END$
+};</s:String>
 	<s:Boolean x:Key="/Default/UserDictionary/Words/=Beatmap/@EntryIndexedValue">True</s:Boolean>
 	<s:Boolean x:Key="/Default/UserDictionary/Words/=beatmaps/@EntryIndexedValue">True</s:Boolean>
 	<s:Boolean x:Key="/Default/UserDictionary/Words/=beatmap_0027s/@EntryIndexedValue">True</s:Boolean>

From aeb2186539bb1cd62971b02ff9839482a9955e3e Mon Sep 17 00:00:00 2001
From: smoogipoo <smoogipoo@smgi.me>
Date: Wed, 26 Dec 2018 16:06:39 +0900
Subject: [PATCH 6/9] Fix api get user request never failing

---
 osu.Game/Online/API/APIAccess.cs | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs
index 8038c1fc1f..10b4e73419 100644
--- a/osu.Game/Online/API/APIAccess.cs
+++ b/osu.Game/Online/API/APIAccess.cs
@@ -145,7 +145,8 @@ namespace osu.Game.Online.API
 
                         if (!handleRequest(userReq))
                         {
-                            Thread.Sleep(500);
+                            if (State == APIState.Connecting)
+                                State = APIState.Failing;
                             continue;
                         }
 

From 63847890d15e64be77da4e4ecf9329711d8e392f Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Wed, 26 Dec 2018 18:05:12 +0900
Subject: [PATCH 7/9] Add better messaging when connecting or failing

---
 .../Graphics/Containers/LinkFlowContainer.cs  | 21 ++++++++++++--
 .../Sections/General/LoginSettings.cs         | 29 +++++++++++--------
 2 files changed, 35 insertions(+), 15 deletions(-)

diff --git a/osu.Game/Graphics/Containers/LinkFlowContainer.cs b/osu.Game/Graphics/Containers/LinkFlowContainer.cs
index 54a2ea47f9..74315d2522 100644
--- a/osu.Game/Graphics/Containers/LinkFlowContainer.cs
+++ b/osu.Game/Graphics/Containers/LinkFlowContainer.cs
@@ -7,6 +7,7 @@ using System.Linq;
 using osu.Framework.Allocation;
 using osu.Framework.Graphics.Sprites;
 using System.Collections.Generic;
+using osu.Framework.Graphics;
 using osu.Framework.Logging;
 using osu.Game.Overlays;
 using osu.Game.Overlays.Notifications;
@@ -61,11 +62,25 @@ namespace osu.Game.Graphics.Containers
         }
 
         public void AddLink(string text, string url, LinkAction linkType = LinkAction.External, string linkArgument = null, string tooltipText = null, Action<SpriteText> creationParameters = null)
+            => createLink(AddText(text, creationParameters), text, url, linkType, linkArgument, tooltipText);
+
+        public void AddLink(string text, Action action, string tooltipText = null, Action<SpriteText> creationParameters = null)
+            => createLink(AddText(text, creationParameters), text, tooltipText: tooltipText, action: action);
+
+        public void AddLink(IEnumerable<SpriteText> text, string url, LinkAction linkType = LinkAction.External, string linkArgument = null, string tooltipText = null)
         {
-            AddInternal(new DrawableLinkCompiler(AddText(text, creationParameters).ToList())
+            foreach (var t in text)
+                AddArbitraryDrawable(t);
+
+            createLink(text, null, url, linkType, linkArgument, tooltipText);
+        }
+
+        private void createLink(IEnumerable<Drawable> drawables, string text, string url = null, LinkAction linkType = LinkAction.External, string linkArgument = null, string tooltipText = null, Action action = null)
+        {
+            AddInternal(new DrawableLinkCompiler(drawables.OfType<SpriteText>().ToList())
             {
                 TooltipText = tooltipText ?? (url != text ? url : string.Empty),
-                Action = () =>
+                Action = action ?? (() =>
                 {
                     switch (linkType)
                     {
@@ -104,7 +119,7 @@ namespace osu.Game.Graphics.Containers
                         default:
                             throw new NotImplementedException($"This {nameof(LinkAction)} ({linkType.ToString()}) is missing an associated action.");
                     }
-                },
+                }),
             });
         }
     }
diff --git a/osu.Game/Overlays/Settings/Sections/General/LoginSettings.cs b/osu.Game/Overlays/Settings/Sections/General/LoginSettings.cs
index 0cb6d2d1aa..163eced103 100644
--- a/osu.Game/Overlays/Settings/Sections/General/LoginSettings.cs
+++ b/osu.Game/Overlays/Settings/Sections/General/LoginSettings.cs
@@ -17,6 +17,7 @@ using osu.Game.Graphics;
 using osuTK.Graphics;
 using osu.Framework.Extensions.Color4Extensions;
 using osu.Framework.Input.Events;
+using osu.Game.Graphics.Containers;
 using RectangleF = osu.Framework.Graphics.Primitives.RectangleF;
 using Container = osu.Framework.Graphics.Containers.Container;
 
@@ -86,25 +87,29 @@ namespace osu.Game.Overlays.Settings.Sections.General
                     };
                     break;
                 case APIState.Failing:
-                    Children = new Drawable[]
-                    {
-                        new OsuSpriteText
-                        {
-                            Text = "Connection failing :(",
-                        },
-                    };
-                    break;
                 case APIState.Connecting:
+                    LinkFlowContainer linkFlow;
+
                     Children = new Drawable[]
                     {
-                        new OsuSpriteText
+                        new LoadingAnimation
                         {
-                            Anchor = Anchor.Centre,
-                            Origin = Anchor.Centre,
-                            Text = "Connecting...",
+                            State = Visibility.Visible,
+                            Anchor = Anchor.TopCentre,
+                            Origin = Anchor.TopCentre,
+                        },
+                        linkFlow = new LinkFlowContainer
+                        {
+                            Anchor = Anchor.TopCentre,
+                            Origin = Anchor.TopCentre,
+                            TextAnchor = Anchor.TopCentre,
+                            AutoSizeAxes = Axes.Both,
+                            Text = state == APIState.Failing ? "Connection is failing, will attempt to reconnect... " : "Attempting to connect... ",
                             Margin = new MarginPadding { Top = 10, Bottom = 10 },
                         },
                     };
+
+                    linkFlow.AddLink("cancel", api.Logout, string.Empty);
                     break;
                 case APIState.Online:
                     Children = new Drawable[]

From edfb027ff274fc50c67c3d9204bfd3ce4067abf3 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Wed, 26 Dec 2018 18:49:33 +0900
Subject: [PATCH 8/9] Update framework

---
 osu.Game/osu.Game.csproj | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj
index 18addaefb6..103c7c20d6 100644
--- a/osu.Game/osu.Game.csproj
+++ b/osu.Game/osu.Game.csproj
@@ -18,7 +18,7 @@
     <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
     <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.0" />
     <PackageReference Include="Newtonsoft.Json" Version="12.0.1" />
-    <PackageReference Include="ppy.osu.Framework" Version="2018.1221.0" />
+    <PackageReference Include="ppy.osu.Framework" Version="2018.1226.0" />
     <PackageReference Include="SharpCompress" Version="0.22.0" />
     <PackageReference Include="NUnit" Version="3.11.0" />
     <PackageReference Include="SharpRaven" Version="2.4.0" />

From ae47eb61caf9ef810cd0d2d56f12164d78f45c05 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Wed, 26 Dec 2018 20:42:47 +0900
Subject: [PATCH 9/9] Fix test not working when not logged in

---
 .../TestCaseUpdateableBeatmapBackgroundSprite.cs     | 12 +++++++++---
 1 file changed, 9 insertions(+), 3 deletions(-)

diff --git a/osu.Game.Tests/Visual/TestCaseUpdateableBeatmapBackgroundSprite.cs b/osu.Game.Tests/Visual/TestCaseUpdateableBeatmapBackgroundSprite.cs
index 24701fccbc..bc449f645b 100644
--- a/osu.Game.Tests/Visual/TestCaseUpdateableBeatmapBackgroundSprite.cs
+++ b/osu.Game.Tests/Visual/TestCaseUpdateableBeatmapBackgroundSprite.cs
@@ -40,12 +40,18 @@ namespace osu.Game.Tests.Visual
             AddStep("imported", () => beatmapBindable.Value = imported.Beatmaps.First());
 
             if (api.IsLoggedIn)
+            {
                 AddUntilStep(() => req.Result != null, "wait for api response");
 
-            AddStep("online", () => beatmapBindable.Value = new BeatmapInfo
+                AddStep("online", () => beatmapBindable.Value = new BeatmapInfo
+                {
+                    BeatmapSet = req.Result?.ToBeatmapSet(rulesets)
+                });
+            }
+            else
             {
-                BeatmapSet = req.Result?.ToBeatmapSet(rulesets)
-            });
+                AddStep("online (login first)", () => { });
+            }
         }
     }
 }