From fb724ca8a705ac6f093be9c96c7e2f30c41b5a07 Mon Sep 17 00:00:00 2001
From: naoey <naoey20@gmail.com>
Date: Wed, 28 Feb 2018 08:32:30 +0530
Subject: [PATCH 01/16] Make song select ensure current beatmap is always
 playable in the active ruleset.

 - Add a to TestCasePlaySongSelect testing this scenario
---
 .../Visual/TestCasePlaySongSelect.cs          | 38 ++++++++++++++---
 osu.Game/Screens/Select/SongSelect.cs         | 41 ++++++++++++++++++-
 2 files changed, 72 insertions(+), 7 deletions(-)

diff --git a/osu.Game.Tests/Visual/TestCasePlaySongSelect.cs b/osu.Game.Tests/Visual/TestCasePlaySongSelect.cs
index 13b2be9fdb..a4086ea2cd 100644
--- a/osu.Game.Tests/Visual/TestCasePlaySongSelect.cs
+++ b/osu.Game.Tests/Visual/TestCasePlaySongSelect.cs
@@ -10,6 +10,7 @@ using osu.Framework.Allocation;
 using osu.Framework.Extensions;
 using osu.Framework.MathUtils;
 using osu.Game.Beatmaps;
+using osu.Game.Configuration;
 using osu.Game.Database;
 using osu.Game.Rulesets;
 using osu.Game.Screens.Select;
@@ -53,10 +54,14 @@ namespace osu.Game.Tests.Visual
             public WorkingBeatmap CurrentBeatmap => Beatmap.Value;
             public WorkingBeatmap CurrentBeatmapDetailsBeatmap => BeatmapDetails.Beatmap;
             public new BeatmapCarousel Carousel => base.Carousel;
+
+            public void SetRuleset(RulesetInfo ruleset) => Ruleset.Value = ruleset;
+
+            public int? RulesetID => Ruleset.Value.ID;
         }
 
         [BackgroundDependencyLoader]
-        private void load(OsuGameBase game)
+        private void load(OsuGameBase game, OsuConfigManager config)
         {
             TestSongSelect songSelect = null;
 
@@ -113,6 +118,24 @@ namespace osu.Game.Tests.Visual
             AddStep(@"Sort by Title", delegate { songSelect.FilterControl.Sort = SortMode.Title; });
             AddStep(@"Sort by Author", delegate { songSelect.FilterControl.Sort = SortMode.Author; });
             AddStep(@"Sort by Difficulty", delegate { songSelect.FilterControl.Sort = SortMode.Difficulty; });
+
+            AddWaitStep(5);
+
+            AddStep(@"Set unplayable WorkingBeatmap", () =>
+            {
+                var testMap = manager.GetAllUsableBeatmapSets().First().Beatmaps.First(b => b.RulesetID != 0);
+                songSelect.SetRuleset(rulesets.AvailableRulesets.First());
+                game.Beatmap.Value = manager.GetWorkingBeatmap(testMap);
+            });
+            AddAssert(@"WorkingBeatmap changed to playable ruleset", () => songSelect.RulesetID == 0 && game.Beatmap.Value.BeatmapInfo.RulesetID == 0);
+            AddStep(@"Disallow beatmap conversion", () =>
+            {
+                config.GetBindable<bool>(OsuSetting.ShowConvertedBeatmaps).Value = false;
+                game.Beatmap.Value = manager.GetWorkingBeatmap(manager.GetAllUsableBeatmapSets().First().Beatmaps.First());
+            });
+            loadNewSongSelect();
+            AddWaitStep(3);
+            AddAssert(@"Ruleset matches beatmap", () => songSelect.RulesetID == game.Beatmap.Value.BeatmapInfo.RulesetID);
         }
 
         private BeatmapSetInfo createTestBeatmapSet(int i)
@@ -134,7 +157,8 @@ namespace osu.Game.Tests.Visual
                     new BeatmapInfo
                     {
                         OnlineBeatmapID = 1234 + i,
-                        Ruleset = rulesets.AvailableRulesets.First(),
+                        Ruleset = rulesets.AvailableRulesets.ElementAt(0),
+                        RulesetID = 0,
                         Path = "normal.osu",
                         Version = "Normal",
                         BaseDifficulty = new BeatmapDifficulty
@@ -145,8 +169,9 @@ namespace osu.Game.Tests.Visual
                     new BeatmapInfo
                     {
                         OnlineBeatmapID = 1235 + i,
-                        Ruleset = rulesets.AvailableRulesets.First(),
-                        Path = "hard.osu",
+                        Ruleset = rulesets.AvailableRulesets.First(r => r.ID != 0),
+                        RulesetID = 1,
+                        Path = "hard.taiko",
                         Version = "Hard",
                         BaseDifficulty = new BeatmapDifficulty
                         {
@@ -156,8 +181,9 @@ namespace osu.Game.Tests.Visual
                     new BeatmapInfo
                     {
                         OnlineBeatmapID = 1236 + i,
-                        Ruleset = rulesets.AvailableRulesets.First(),
-                        Path = "insane.osu",
+                        Ruleset = rulesets.AvailableRulesets.ElementAt(2),
+                        RulesetID = 2,
+                        Path = "insane.fruits",
                         Version = "Insane",
                         BaseDifficulty = new BeatmapDifficulty
                         {
diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs
index de6847d866..6e1d95d42e 100644
--- a/osu.Game/Screens/Select/SongSelect.cs
+++ b/osu.Game/Screens/Select/SongSelect.cs
@@ -2,6 +2,7 @@
 // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
 
 using System;
+using System.Linq;
 using System.Threading;
 using OpenTK;
 using OpenTK.Input;
@@ -9,12 +10,14 @@ using osu.Framework.Allocation;
 using osu.Framework.Audio;
 using osu.Framework.Audio.Sample;
 using osu.Framework.Audio.Track;
+using osu.Framework.Configuration;
 using osu.Framework.Graphics;
 using osu.Framework.Graphics.Containers;
 using osu.Framework.Input;
 using osu.Framework.Screens;
 using osu.Framework.Threading;
 using osu.Game.Beatmaps;
+using osu.Game.Configuration;
 using osu.Game.Graphics;
 using osu.Game.Graphics.Containers;
 using osu.Game.Overlays;
@@ -63,6 +66,8 @@ namespace osu.Game.Screens.Select
         private SampleChannel sampleChangeDifficulty;
         private SampleChannel sampleChangeBeatmap;
 
+        private Bindable<bool> rulesetConversionAllowed;
+
         private CancellationTokenSource initialAddSetsTask;
 
         private DependencyContainer dependencies;
@@ -179,7 +184,7 @@ namespace osu.Game.Screens.Select
         }
 
         [BackgroundDependencyLoader(permitNulls: true)]
-        private void load(BeatmapManager beatmaps, AudioManager audio, DialogOverlay dialog, OsuGame osu, OsuColour colours)
+        private void load(BeatmapManager beatmaps, AudioManager audio, DialogOverlay dialog, OsuGame osu, OsuColour colours, OsuConfigManager config)
         {
             dependencies.CacheAs(this);
 
@@ -194,6 +199,8 @@ namespace osu.Game.Screens.Select
             if (this.beatmaps == null)
                 this.beatmaps = beatmaps;
 
+            rulesetConversionAllowed = config.GetBindable<bool>(OsuSetting.ShowConvertedBeatmaps);
+
             if (osu != null)
                 Ruleset.BindTo(osu.Ruleset);
 
@@ -217,7 +224,10 @@ namespace osu.Game.Screens.Select
             Beatmap.ValueChanged += b =>
             {
                 if (IsCurrentScreen)
+                {
                     Carousel.SelectBeatmap(b?.BeatmapInfo);
+                    ensurePlayableRuleset();
+                }
             };
         }
 
@@ -316,6 +326,7 @@ namespace osu.Game.Screens.Select
         {
             base.OnEntering(last);
 
+            ensurePlayableRuleset();
             Content.FadeInFromZero(250);
             FilterControl.Activate();
         }
@@ -441,6 +452,34 @@ namespace osu.Game.Screens.Select
             }
         }
 
+        private void ensurePlayableRuleset()
+        {
+            if (Beatmap.IsDefault)
+                // DummyBeatmap won't be playable anyway
+                return;
+
+            bool conversionAllowed = rulesetConversionAllowed.Value;
+            int? currentRuleset = Ruleset.Value.ID;
+            int beatmapRuleset = Beatmap.Value.BeatmapInfo.RulesetID;
+
+            if (currentRuleset == beatmapRuleset || conversionAllowed && beatmapRuleset == 0)
+                // Current beatmap is playable, nothing more to do
+                return;
+
+            // Otherwise, first check if the current beatmapset has any playable beatmaps
+            BeatmapInfo beatmap = Beatmap.Value.BeatmapSetInfo.Beatmaps?.FirstOrDefault(b => b.RulesetID == currentRuleset || conversionAllowed && b.RulesetID == 0);
+
+            // If it does then update the WorkingBeatmap
+            if (beatmap != null)
+            {
+                Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmap);
+                return;
+            }
+
+            // If it doesn't, then update the current ruleset so that the current beatmap is playable
+            Ruleset.Value = Beatmap.Value.BeatmapInfo.Ruleset;
+        }
+
         private void onBeatmapSetAdded(BeatmapSetInfo s) => Carousel.UpdateBeatmapSet(s);
         private void onBeatmapSetRemoved(BeatmapSetInfo s) => Carousel.RemoveBeatmapSet(s);
         private void onBeatmapRestored(BeatmapInfo b) => Carousel.UpdateBeatmapSet(beatmaps.QueryBeatmapSet(s => s.ID == b.BeatmapSetInfoID));

From a57dc154f9aa323c6ab367442c417b7bb48363ae Mon Sep 17 00:00:00 2001
From: naoey <naoey20@gmail.com>
Date: Sat, 3 Mar 2018 19:54:54 +0530
Subject: [PATCH 02/16] More specific tests.

---
 .../Visual/TestCasePlaySongSelect.cs          | 80 ++++++++++++++++---
 osu.Game/Screens/Select/SongSelect.cs         | 18 +++--
 2 files changed, 81 insertions(+), 17 deletions(-)

diff --git a/osu.Game.Tests/Visual/TestCasePlaySongSelect.cs b/osu.Game.Tests/Visual/TestCasePlaySongSelect.cs
index d895080afe..8532962389 100644
--- a/osu.Game.Tests/Visual/TestCasePlaySongSelect.cs
+++ b/osu.Game.Tests/Visual/TestCasePlaySongSelect.cs
@@ -60,6 +60,14 @@ namespace osu.Game.Tests.Visual
             public void SetRuleset(RulesetInfo ruleset) => Ruleset.Value = ruleset;
 
             public int? RulesetID => Ruleset.Value.ID;
+
+            protected override void Dispose(bool isDisposing)
+            {
+                base.Dispose(isDisposing);
+
+                // Necessary while running tests because gc is moody and uncollected object interferes with OnEntering test
+                Beatmap.ValueChanged -= WorkingBeatmapChanged;
+            }
         }
 
         [BackgroundDependencyLoader]
@@ -82,6 +90,7 @@ namespace osu.Game.Tests.Visual
             {
                 if (deleteMaps)
                 {
+                    // TODO: check why this alone doesn't allow import test to run twice in the same session, probably because the delete op is not saved?
                     manager.Delete(manager.GetAllUsableBeatmapSets());
                     game.Beatmap.SetDefault();
                 }
@@ -93,6 +102,8 @@ namespace osu.Game.Tests.Visual
                 }
 
                 Add(songSelect = new TestSongSelect());
+
+                songSelect?.SetRuleset(rulesets.AvailableRulesets.First());
             });
 
             loadNewSongSelect(true);
@@ -107,6 +118,36 @@ namespace osu.Game.Tests.Visual
             {
                 for (int i = 0; i < 100; i += 10)
                     manager.Import(createTestBeatmapSet(i));
+
+                // also import a set which has a single non - osu ruleset beatmap
+                manager.Import(new BeatmapSetInfo
+                {
+                    OnlineBeatmapSetID = 1993,
+                    Hash = new MemoryStream(Encoding.UTF8.GetBytes(Guid.NewGuid().ToString())).ComputeMD5Hash(),
+                    Metadata = new BeatmapMetadata
+                    {
+                        OnlineBeatmapSetID = 1993,
+                        // Create random metadata, then we can check if sorting works based on these
+                        Artist = "MONACA " + RNG.Next(0, 9),
+                        Title = "Black Song " + RNG.Next(0, 9),
+                        AuthorString = "Some Guy " + RNG.Next(0, 9),
+                    },
+                    Beatmaps = new List<BeatmapInfo>
+                    {
+                        new BeatmapInfo
+                        {
+                            OnlineBeatmapID = 1994,
+                            Ruleset = rulesets.AvailableRulesets.ElementAt(3),
+                            RulesetID = 3,
+                            Path = "normal.fruits",
+                            Version = "Normal",
+                            BaseDifficulty = new BeatmapDifficulty
+                            {
+                                OverallDifficulty = 3.5f,
+                            }
+                        },
+                    }
+                });
             });
 
             AddWaitStep(3);
@@ -121,23 +162,44 @@ namespace osu.Game.Tests.Visual
             AddStep(@"Sort by Author", delegate { songSelect.FilterControl.Sort = SortMode.Author; });
             AddStep(@"Sort by Difficulty", delegate { songSelect.FilterControl.Sort = SortMode.Difficulty; });
 
-            AddWaitStep(5);
+            // Test that song select sets a playable beatmap while entering
+            AddStep(@"Remove song select", () =>
+            {
+                Remove(songSelect);
+                songSelect.Dispose();
+                songSelect = null;
+            });
+            AddStep(@"Set non-osu beatmap", () => game.Beatmap.Value = manager.GetWorkingBeatmap(manager.GetAllUsableBeatmapSets().First().Beatmaps.First(b => b.RulesetID != 0)));
+            AddAssert(@"Non-osu beatmap set", () => game.Beatmap.Value.BeatmapInfo.RulesetID != 0);
+            loadNewSongSelect();
+            AddWaitStep(3);
+            AddAssert(@"osu beatmap set", () => game.Beatmap.Value.BeatmapInfo.RulesetID == 0);
 
-            AddStep(@"Set unplayable WorkingBeatmap", () =>
+            // Test that song select changes WorkingBeatmap to be playable in current ruleset when updated externally
+            AddStep(@"Try set non-osu beatmap", () =>
             {
                 var testMap = manager.GetAllUsableBeatmapSets().First().Beatmaps.First(b => b.RulesetID != 0);
                 songSelect.SetRuleset(rulesets.AvailableRulesets.First());
                 game.Beatmap.Value = manager.GetWorkingBeatmap(testMap);
             });
-            AddAssert(@"WorkingBeatmap changed to playable ruleset", () => songSelect.RulesetID == 0 && game.Beatmap.Value.BeatmapInfo.RulesetID == 0);
-            AddStep(@"Disallow beatmap conversion", () =>
+            AddAssert(@"Beatmap changed to osu", () => songSelect.RulesetID == 0 && game.Beatmap.Value.BeatmapInfo.RulesetID == 0);
+
+            // Test that song select updates WorkingBeatmap when ruleset conversion is disabled
+            AddStep(@"Disable beatmap conversion", () => config.Set(OsuSetting.ShowConvertedBeatmaps, false));
+            AddStep(@"Set osu beatmap taiko rs", () =>
             {
-                config.GetBindable<bool>(OsuSetting.ShowConvertedBeatmaps).Value = false;
-                game.Beatmap.Value = manager.GetWorkingBeatmap(manager.GetAllUsableBeatmapSets().First().Beatmaps.First());
+                game.Beatmap.Value = manager.GetWorkingBeatmap(manager.GetAllUsableBeatmapSets().First().Beatmaps.First(b => b.RulesetID == 0));
+                songSelect.SetRuleset(rulesets.AvailableRulesets.First(r => r.ID == 1));
             });
-            loadNewSongSelect();
-            AddWaitStep(3);
-            AddAssert(@"Ruleset matches beatmap", () => songSelect.RulesetID == game.Beatmap.Value.BeatmapInfo.RulesetID);
+            AddAssert(@"taiko beatmap set", () => songSelect.RulesetID == 1);
+
+            // Test that song select changes the active ruleset when externally set beatmapset has no playable beatmaps
+            AddStep(@"Set fruits only beatmapset", () =>
+            {
+                songSelect.SetRuleset(rulesets.AvailableRulesets.First());
+                game.Beatmap.Value = manager.GetWorkingBeatmap(manager.QueryBeatmapSet(b => b.OnlineBeatmapSetID == 1993).Beatmaps.First());
+            });
+            AddAssert(@"Ruleset changed to fruits", () => songSelect.RulesetID == game.Beatmap.Value.BeatmapInfo.RulesetID);
         }
 
         private BeatmapSetInfo createTestBeatmapSet(int i)
diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs
index 8033f8da8b..d4fd64dcd9 100644
--- a/osu.Game/Screens/Select/SongSelect.cs
+++ b/osu.Game/Screens/Select/SongSelect.cs
@@ -221,14 +221,7 @@ namespace osu.Game.Screens.Select
             Beatmap.DisabledChanged += disabled => Carousel.AllowSelection = !disabled;
             Beatmap.TriggerChange();
 
-            Beatmap.ValueChanged += b =>
-            {
-                if (IsCurrentScreen)
-                {
-                    Carousel.SelectBeatmap(b?.BeatmapInfo);
-                    ensurePlayableRuleset();
-                }
-            };
+            Beatmap.ValueChanged += WorkingBeatmapChanged;
         }
 
         public void Edit(BeatmapInfo beatmap)
@@ -271,6 +264,15 @@ namespace osu.Game.Screens.Select
         // We need to keep track of the last selected beatmap ignoring debounce to play the correct selection sounds.
         private BeatmapInfo beatmapNoDebounce;
 
+        protected void WorkingBeatmapChanged(WorkingBeatmap beatmap)
+        {
+            if (IsCurrentScreen)
+            {
+                Carousel.SelectBeatmap(beatmap?.BeatmapInfo);
+                ensurePlayableRuleset();
+            }
+        }
+
         /// <summary>
         /// selection has been changed as the result of interaction with the carousel.
         /// </summary>

From c6c55c40edf51afef87b5b1531e7da02ebccdfaa Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Sun, 4 Mar 2018 00:50:41 +0900
Subject: [PATCH 03/16] Rewrite BreakOverlay

No longer relies on Schedule calls (could not be rewound).
Also no longer sucks.
---
 osu.Game.Tests/Visual/TestCaseBreakOverlay.cs |   2 +-
 .../{BreaksOverlay => Break}/BlurredIcon.cs   |  10 +-
 .../ArrowsOverlay.cs => Break/BreakArrows.cs} |  35 ++---
 .../InfoContainer.cs => Break/BreakInfo.cs}   |  26 ++--
 .../InfoLine.cs => Break/BreakInfoLine.cs}    |  10 +-
 .../Play/{BreaksOverlay => Break}/GlowIcon.cs |  12 +-
 .../LetterboxOverlay.cs                       |  13 +-
 .../RemainingTimeCounter.cs                   |  14 +-
 .../Play/{BreaksOverlay => }/BreakOverlay.cs  | 142 +++++++++---------
 osu.Game/Screens/Play/Player.cs               |   1 -
 osu.Game/osu.Game.csproj                      |  16 +-
 11 files changed, 125 insertions(+), 156 deletions(-)
 rename osu.Game/Screens/Play/{BreaksOverlay => Break}/BlurredIcon.cs (92%)
 rename osu.Game/Screens/Play/{BreaksOverlay/ArrowsOverlay.cs => Break/BreakArrows.cs} (78%)
 rename osu.Game/Screens/Play/{BreaksOverlay/InfoContainer.cs => Break/BreakInfo.cs} (62%)
 rename osu.Game/Screens/Play/{BreaksOverlay/InfoLine.cs => Break/BreakInfoLine.cs} (84%)
 rename osu.Game/Screens/Play/{BreaksOverlay => Break}/GlowIcon.cs (93%)
 rename osu.Game/Screens/Play/{BreaksOverlay => Break}/LetterboxOverlay.cs (77%)
 rename osu.Game/Screens/Play/{BreaksOverlay => Break}/RemainingTimeCounter.cs (70%)
 rename osu.Game/Screens/Play/{BreaksOverlay => }/BreakOverlay.cs (51%)

diff --git a/osu.Game.Tests/Visual/TestCaseBreakOverlay.cs b/osu.Game.Tests/Visual/TestCaseBreakOverlay.cs
index ae24d86325..51b8c61963 100644
--- a/osu.Game.Tests/Visual/TestCaseBreakOverlay.cs
+++ b/osu.Game.Tests/Visual/TestCaseBreakOverlay.cs
@@ -3,9 +3,9 @@
 
 using osu.Framework.Timing;
 using osu.Game.Beatmaps.Timing;
-using osu.Game.Screens.Play.BreaksOverlay;
 using System.Collections.Generic;
 using NUnit.Framework;
+using osu.Game.Screens.Play;
 
 namespace osu.Game.Tests.Visual
 {
diff --git a/osu.Game/Screens/Play/BreaksOverlay/BlurredIcon.cs b/osu.Game/Screens/Play/Break/BlurredIcon.cs
similarity index 92%
rename from osu.Game/Screens/Play/BreaksOverlay/BlurredIcon.cs
rename to osu.Game/Screens/Play/Break/BlurredIcon.cs
index 5395d7688e..6f47c97f89 100644
--- a/osu.Game/Screens/Play/BreaksOverlay/BlurredIcon.cs
+++ b/osu.Game/Screens/Play/Break/BlurredIcon.cs
@@ -1,13 +1,13 @@
 // Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
 // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
 
-using OpenTK;
-using osu.Framework.Graphics.Containers;
-using osu.Framework.Graphics;
-using osu.Game.Graphics;
 using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Graphics;
+using OpenTK;
 
-namespace osu.Game.Screens.Play.BreaksOverlay
+namespace osu.Game.Screens.Play.Break
 {
     public class BlurredIcon : BufferedContainer
     {
diff --git a/osu.Game/Screens/Play/BreaksOverlay/ArrowsOverlay.cs b/osu.Game/Screens/Play/Break/BreakArrows.cs
similarity index 78%
rename from osu.Game/Screens/Play/BreaksOverlay/ArrowsOverlay.cs
rename to osu.Game/Screens/Play/Break/BreakArrows.cs
index 9fdf90bd28..f2a60cdddf 100644
--- a/osu.Game/Screens/Play/BreaksOverlay/ArrowsOverlay.cs
+++ b/osu.Game/Screens/Play/Break/BreakArrows.cs
@@ -1,18 +1,15 @@
 // 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.Graphics.Containers;
 using osu.Framework.Graphics;
-using OpenTK;
+using osu.Framework.Graphics.Containers;
 using osu.Game.Graphics.Containers;
-using osu.Game.Beatmaps.Timing;
+using OpenTK;
 
-namespace osu.Game.Screens.Play.BreaksOverlay
+namespace osu.Game.Screens.Play.Break
 {
-    public class ArrowsOverlay : VisibilityContainer
+    public class BreakArrows : CompositeDrawable
     {
-        private const double fade_duration = BreakPeriod.MIN_BREAK_DURATION / 2;
-
         private const int glow_icon_size = 60;
         private const int glow_icon_blur_sigma = 10;
         private const float glow_icon_final_offset = 0.22f;
@@ -29,10 +26,10 @@ namespace osu.Game.Screens.Play.BreaksOverlay
         private readonly BlurredIcon leftBlurredIcon;
         private readonly BlurredIcon rightBlurredIcon;
 
-        public ArrowsOverlay()
+        public BreakArrows()
         {
             RelativeSizeAxes = Axes.Both;
-            Children = new Drawable[]
+            InternalChildren = new Drawable[]
             {
                 leftGlowIcon = new GlowIcon
                 {
@@ -82,22 +79,22 @@ namespace osu.Game.Screens.Play.BreaksOverlay
             };
         }
 
-        protected override void PopIn()
+        public void Show(double duration)
         {
-            leftGlowIcon.MoveToX(-glow_icon_final_offset, fade_duration, Easing.OutQuint);
-            rightGlowIcon.MoveToX(glow_icon_final_offset, fade_duration, Easing.OutQuint);
+            leftGlowIcon.MoveToX(-glow_icon_final_offset, duration, Easing.OutQuint);
+            rightGlowIcon.MoveToX(glow_icon_final_offset, duration, Easing.OutQuint);
 
-            leftBlurredIcon.MoveToX(-blurred_icon_final_offset, fade_duration, Easing.OutQuint);
-            rightBlurredIcon.MoveToX(blurred_icon_final_offset, fade_duration, Easing.OutQuint);
+            leftBlurredIcon.MoveToX(-blurred_icon_final_offset, duration, Easing.OutQuint);
+            rightBlurredIcon.MoveToX(blurred_icon_final_offset, duration, Easing.OutQuint);
         }
 
-        protected override void PopOut()
+        public void Hide(double duration)
         {
-            leftGlowIcon.MoveToX(-glow_icon_offscreen_offset, fade_duration, Easing.OutQuint);
-            rightGlowIcon.MoveToX(glow_icon_offscreen_offset, fade_duration, Easing.OutQuint);
+            leftGlowIcon.MoveToX(-glow_icon_offscreen_offset, duration, Easing.OutQuint);
+            rightGlowIcon.MoveToX(glow_icon_offscreen_offset, duration, Easing.OutQuint);
 
-            leftBlurredIcon.MoveToX(-blurred_icon_offscreen_offset, fade_duration, Easing.OutQuint);
-            rightBlurredIcon.MoveToX(blurred_icon_offscreen_offset, fade_duration, Easing.OutQuint);
+            leftBlurredIcon.MoveToX(-blurred_icon_offscreen_offset, duration, Easing.OutQuint);
+            rightBlurredIcon.MoveToX(blurred_icon_offscreen_offset, duration, Easing.OutQuint);
         }
     }
 }
diff --git a/osu.Game/Screens/Play/BreaksOverlay/InfoContainer.cs b/osu.Game/Screens/Play/Break/BreakInfo.cs
similarity index 62%
rename from osu.Game/Screens/Play/BreaksOverlay/InfoContainer.cs
rename to osu.Game/Screens/Play/Break/BreakInfo.cs
index d7ab4ff2e5..5e011903fe 100644
--- a/osu.Game/Screens/Play/BreaksOverlay/InfoContainer.cs
+++ b/osu.Game/Screens/Play/Break/BreakInfo.cs
@@ -1,24 +1,21 @@
 // Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
 // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
 
-using OpenTK;
 using osu.Framework.Graphics;
 using osu.Framework.Graphics.Containers;
 using osu.Game.Graphics.Sprites;
 using osu.Game.Rulesets.Scoring;
-using osu.Game.Beatmaps.Timing;
+using OpenTK;
 
-namespace osu.Game.Screens.Play.BreaksOverlay
+namespace osu.Game.Screens.Play.Break
 {
-    public class InfoContainer : VisibilityContainer
+    public class BreakInfo : Container
     {
-        private const double fade_duration = BreakPeriod.MIN_BREAK_DURATION / 2;
+        public PercentageBreakInfoLine AccuracyDisplay;
+        public BreakInfoLine<int> RankDisplay;
+        public BreakInfoLine<ScoreRank> GradeDisplay;
 
-        public PercentageInfoLine AccuracyDisplay;
-        public InfoLine<int> RankDisplay;
-        public InfoLine<ScoreRank> GradeDisplay;
-
-        public InfoContainer()
+        public BreakInfo()
         {
             AutoSizeAxes = Axes.Both;
             Child = new FillFlowContainer
@@ -43,16 +40,13 @@ namespace osu.Game.Screens.Play.BreaksOverlay
                         Direction = FillDirection.Vertical,
                         Children = new Drawable[]
                         {
-                            AccuracyDisplay = new PercentageInfoLine("Accuracy"),
-                            RankDisplay = new InfoLine<int>("Rank"),
-                            GradeDisplay = new InfoLine<ScoreRank>("Grade"),
+                            AccuracyDisplay = new PercentageBreakInfoLine("Accuracy"),
+                            RankDisplay = new BreakInfoLine<int>("Rank"),
+                            GradeDisplay = new BreakInfoLine<ScoreRank>("Grade"),
                         },
                     }
                 },
             };
         }
-
-        protected override void PopIn() => this.FadeIn(fade_duration);
-        protected override void PopOut() => this.FadeOut(fade_duration);
     }
 }
diff --git a/osu.Game/Screens/Play/BreaksOverlay/InfoLine.cs b/osu.Game/Screens/Play/Break/BreakInfoLine.cs
similarity index 84%
rename from osu.Game/Screens/Play/BreaksOverlay/InfoLine.cs
rename to osu.Game/Screens/Play/Break/BreakInfoLine.cs
index b39eaf1c22..3d96bca1fa 100644
--- a/osu.Game/Screens/Play/BreaksOverlay/InfoLine.cs
+++ b/osu.Game/Screens/Play/Break/BreakInfoLine.cs
@@ -8,9 +8,9 @@ using osu.Framework.Graphics.Containers;
 using osu.Game.Graphics;
 using osu.Game.Graphics.Sprites;
 
-namespace osu.Game.Screens.Play.BreaksOverlay
+namespace osu.Game.Screens.Play.Break
 {
-    public class InfoLine<T> : Container
+    public class BreakInfoLine<T> : Container
         where T : struct
     {
         private const int margin = 2;
@@ -22,7 +22,7 @@ namespace osu.Game.Screens.Play.BreaksOverlay
 
         private readonly string prefix;
 
-        public InfoLine(string name, string prefix = @"")
+        public BreakInfoLine(string name, string prefix = @"")
         {
             this.prefix = prefix;
 
@@ -71,9 +71,9 @@ namespace osu.Game.Screens.Play.BreaksOverlay
         }
     }
 
-    public class PercentageInfoLine : InfoLine<double>
+    public class PercentageBreakInfoLine : BreakInfoLine<double>
     {
-        public PercentageInfoLine(string name, string prefix = "") : base(name, prefix)
+        public PercentageBreakInfoLine(string name, string prefix = "") : base(name, prefix)
         {
         }
 
diff --git a/osu.Game/Screens/Play/BreaksOverlay/GlowIcon.cs b/osu.Game/Screens/Play/Break/GlowIcon.cs
similarity index 93%
rename from osu.Game/Screens/Play/BreaksOverlay/GlowIcon.cs
rename to osu.Game/Screens/Play/Break/GlowIcon.cs
index bad9df2093..79b39a873a 100644
--- a/osu.Game/Screens/Play/BreaksOverlay/GlowIcon.cs
+++ b/osu.Game/Screens/Play/Break/GlowIcon.cs
@@ -1,13 +1,13 @@
 // 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.Graphics.Containers;
+using osu.Framework.Allocation;
 using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
 using osu.Game.Graphics;
 using OpenTK;
-using osu.Framework.Allocation;
 
-namespace osu.Game.Screens.Play.BreaksOverlay
+namespace osu.Game.Screens.Play.Break
 {
     public class GlowIcon : Container
     {
@@ -16,24 +16,24 @@ namespace osu.Game.Screens.Play.BreaksOverlay
 
         public override Vector2 Size
         {
+            get { return base.Size; }
             set
             {
                 blurredIcon.Size = spriteIcon.Size = value;
                 blurredIcon.ForceRedraw();
             }
-            get { return base.Size; }
         }
 
         public Vector2 BlurSigma
         {
-            set { blurredIcon.BlurSigma = value; }
             get { return blurredIcon.BlurSigma; }
+            set { blurredIcon.BlurSigma = value; }
         }
 
         public FontAwesome Icon
         {
-            set { spriteIcon.Icon = blurredIcon.Icon = value; }
             get { return spriteIcon.Icon; }
+            set { spriteIcon.Icon = blurredIcon.Icon = value; }
         }
 
         public GlowIcon()
diff --git a/osu.Game/Screens/Play/BreaksOverlay/LetterboxOverlay.cs b/osu.Game/Screens/Play/Break/LetterboxOverlay.cs
similarity index 77%
rename from osu.Game/Screens/Play/BreaksOverlay/LetterboxOverlay.cs
rename to osu.Game/Screens/Play/Break/LetterboxOverlay.cs
index f4c9362fff..21eb5ebea0 100644
--- a/osu.Game/Screens/Play/BreaksOverlay/LetterboxOverlay.cs
+++ b/osu.Game/Screens/Play/Break/LetterboxOverlay.cs
@@ -1,18 +1,16 @@
 // Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
 // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
 
-using OpenTK.Graphics;
 using osu.Framework.Graphics;
 using osu.Framework.Graphics.Colour;
 using osu.Framework.Graphics.Containers;
 using osu.Framework.Graphics.Shapes;
-using osu.Game.Beatmaps.Timing;
+using OpenTK.Graphics;
 
-namespace osu.Game.Screens.Play.BreaksOverlay
+namespace osu.Game.Screens.Play.Break
 {
-    public class LetterboxOverlay : VisibilityContainer
+    public class LetterboxOverlay : CompositeDrawable
     {
-        private const double fade_duration = BreakPeriod.MIN_BREAK_DURATION / 2;
         private const int height = 350;
 
         private static readonly Color4 transparent_black = new Color4(0, 0, 0, 0);
@@ -20,7 +18,7 @@ namespace osu.Game.Screens.Play.BreaksOverlay
         public LetterboxOverlay()
         {
             RelativeSizeAxes = Axes.Both;
-            Children = new Drawable[]
+            InternalChildren = new Drawable[]
             {
                 new Container
                 {
@@ -48,8 +46,5 @@ namespace osu.Game.Screens.Play.BreaksOverlay
                 }
             };
         }
-
-        protected override void PopIn() => this.FadeIn(fade_duration);
-        protected override void PopOut() => this.FadeOut(fade_duration);
     }
 }
diff --git a/osu.Game/Screens/Play/BreaksOverlay/RemainingTimeCounter.cs b/osu.Game/Screens/Play/Break/RemainingTimeCounter.cs
similarity index 70%
rename from osu.Game/Screens/Play/BreaksOverlay/RemainingTimeCounter.cs
rename to osu.Game/Screens/Play/Break/RemainingTimeCounter.cs
index 015fefb423..f6e683f519 100644
--- a/osu.Game/Screens/Play/BreaksOverlay/RemainingTimeCounter.cs
+++ b/osu.Game/Screens/Play/Break/RemainingTimeCounter.cs
@@ -1,18 +1,15 @@
 // Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
 // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
 
-using osu.Game.Graphics.Sprites;
-using osu.Framework.Graphics;
 using System;
-using osu.Game.Beatmaps.Timing;
+using osu.Framework.Graphics;
 using osu.Framework.Graphics.UserInterface;
+using osu.Game.Graphics.Sprites;
 
-namespace osu.Game.Screens.Play.BreaksOverlay
+namespace osu.Game.Screens.Play.Break
 {
     public class RemainingTimeCounter : Counter
     {
-        private const double fade_duration = BreakPeriod.MIN_BREAK_DURATION / 2;
-
         private readonly OsuSpriteText counter;
 
         public RemainingTimeCounter()
@@ -25,13 +22,8 @@ namespace osu.Game.Screens.Play.BreaksOverlay
                 TextSize = 33,
                 Font = "Venera",
             };
-
-            Alpha = 0;
         }
 
         protected override void OnCountChanged(double count) => counter.Text = ((int)Math.Ceiling(count / 1000)).ToString();
-
-        public override void Show() => this.FadeIn(fade_duration);
-        public override void Hide() => this.FadeOut(fade_duration);
     }
 }
diff --git a/osu.Game/Screens/Play/BreaksOverlay/BreakOverlay.cs b/osu.Game/Screens/Play/BreakOverlay.cs
similarity index 51%
rename from osu.Game/Screens/Play/BreaksOverlay/BreakOverlay.cs
rename to osu.Game/Screens/Play/BreakOverlay.cs
index af7c1ef5aa..6c7ee596a1 100644
--- a/osu.Game/Screens/Play/BreaksOverlay/BreakOverlay.cs
+++ b/osu.Game/Screens/Play/BreakOverlay.cs
@@ -1,15 +1,16 @@
 // Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
 // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
 
+using System.Collections.Generic;
 using osu.Framework.Graphics;
 using osu.Framework.Graphics.Containers;
 using osu.Framework.Graphics.Shapes;
+using osu.Framework.Graphics.UserInterface;
 using osu.Game.Beatmaps.Timing;
 using osu.Game.Rulesets.Scoring;
-using System.Collections.Generic;
-using osu.Framework.Graphics.UserInterface;
+using osu.Game.Screens.Play.Break;
 
-namespace osu.Game.Screens.Play.BreaksOverlay
+namespace osu.Game.Screens.Play
 {
     public class BreakOverlay : Container
     {
@@ -18,28 +19,26 @@ namespace osu.Game.Screens.Play.BreaksOverlay
         private const int vertical_margin = 25;
 
         private List<BreakPeriod> breaks;
+
+        private readonly Container fadeContainer;
+
         public List<BreakPeriod> Breaks
         {
+            get => breaks;
             set
             {
                 breaks = value;
                 initializeBreaks();
             }
-            get
-            {
-                return breaks;
-            }
         }
 
         public override bool RemoveCompletedTransforms => false;
 
-        private readonly bool letterboxing;
-        private readonly LetterboxOverlay letterboxOverlay;
         private readonly Container remainingTimeAdjustmentBox;
         private readonly Container remainingTimeBox;
         private readonly RemainingTimeCounter remainingTimeCounter;
-        private readonly InfoContainer info;
-        private readonly ArrowsOverlay arrowsOverlay;
+        private readonly BreakInfo info;
+        private readonly BreakArrows breakArrows;
 
         public BreakOverlay(bool letterboxing, ScoreProcessor scoreProcessor)
             : this(letterboxing)
@@ -49,61 +48,72 @@ namespace osu.Game.Screens.Play.BreaksOverlay
 
         public BreakOverlay(bool letterboxing)
         {
-            this.letterboxing = letterboxing;
-
             RelativeSizeAxes = Axes.Both;
-            Children = new Drawable[]
+            Child = fadeContainer = new Container
             {
-                letterboxOverlay = new LetterboxOverlay
+                Alpha = 0,
+                RelativeSizeAxes = Axes.Both,
+                Children = new Drawable[]
                 {
-                    Anchor = Anchor.Centre,
-                    Origin = Anchor.Centre,
-                },
-                remainingTimeAdjustmentBox = new Container
-                {
-                    Anchor = Anchor.Centre,
-                    Origin = Anchor.Centre,
-                    AutoSizeAxes = Axes.Y,
-                    RelativeSizeAxes = Axes.X,
-                    Width = 0,
-                    Child = remainingTimeBox = new Container
+                    new LetterboxOverlay
+                    {
+                        Alpha = letterboxing ? 1 : 0,
+                        Anchor = Anchor.Centre,
+                        Origin = Anchor.Centre,
+                    },
+                    remainingTimeAdjustmentBox = new Container
                     {
                         Anchor = Anchor.Centre,
                         Origin = Anchor.Centre,
+                        AutoSizeAxes = Axes.Y,
                         RelativeSizeAxes = Axes.X,
-                        Height = 8,
-                        CornerRadius = 4,
-                        Masking = true,
-                        Child = new Box { RelativeSizeAxes = Axes.Both }
+                        Width = 0,
+                        Child = remainingTimeBox = new Container
+                        {
+                            Anchor = Anchor.Centre,
+                            Origin = Anchor.Centre,
+                            RelativeSizeAxes = Axes.X,
+                            Height = 8,
+                            CornerRadius = 4,
+                            Masking = true,
+                            Child = new Box { RelativeSizeAxes = Axes.Both }
+                        }
+                    },
+                    remainingTimeCounter = new RemainingTimeCounter
+                    {
+                        Anchor = Anchor.Centre,
+                        Origin = Anchor.BottomCentre,
+                        Margin = new MarginPadding { Bottom = vertical_margin },
+                    },
+                    info = new BreakInfo
+                    {
+                        Anchor = Anchor.Centre,
+                        Origin = Anchor.TopCentre,
+                        Margin = new MarginPadding { Top = vertical_margin },
+                    },
+                    breakArrows = new BreakArrows
+                    {
+                        Anchor = Anchor.Centre,
+                        Origin = Anchor.Centre,
                     }
-                },
-                remainingTimeCounter = new RemainingTimeCounter
-                {
-                    Anchor = Anchor.Centre,
-                    Origin = Anchor.BottomCentre,
-                    Margin = new MarginPadding { Bottom = vertical_margin },
-                },
-                info = new InfoContainer
-                {
-                    Anchor = Anchor.Centre,
-                    Origin = Anchor.TopCentre,
-                    Margin = new MarginPadding { Top = vertical_margin },
-                },
-                arrowsOverlay = new ArrowsOverlay
-                {
-                    Anchor = Anchor.Centre,
-                    Origin = Anchor.Centre,
                 }
             };
         }
 
+        protected override void LoadComplete()
+        {
+            base.LoadComplete();
+            initializeBreaks();
+        }
+
         private void initializeBreaks()
         {
+            if (!IsLoaded) return; // we need a clock.
+
             FinishTransforms(true);
             Scheduler.CancelDelayedTasks();
 
-            if (breaks == null)
-                return;
+            if (breaks == null) return; //we need breaks.
 
             foreach (var b in breaks)
             {
@@ -112,6 +122,9 @@ namespace osu.Game.Screens.Play.BreaksOverlay
 
                 using (BeginAbsoluteSequence(b.StartTime, true))
                 {
+                    fadeContainer.FadeIn(fade_duration);
+                    breakArrows.Show(fade_duration);
+
                     remainingTimeAdjustmentBox
                         .ResizeWidthTo(remaining_time_container_max_size, fade_duration, Easing.OutQuint)
                         .Delay(b.Duration - fade_duration)
@@ -123,37 +136,16 @@ namespace osu.Game.Screens.Play.BreaksOverlay
                         .ResizeWidthTo(1);
 
                     remainingTimeCounter.CountTo(b.Duration).CountTo(0, b.Duration);
-                }
 
-                using (BeginAbsoluteSequence(b.StartTime))
-                {
-                    Schedule(showBreak);
-                    using (BeginDelayedSequence(b.Duration - fade_duration))
-                        Schedule(hideBreak);
+                    using (BeginDelayedSequence(b.Duration - fade_duration, true))
+                    {
+                        fadeContainer.FadeOut(fade_duration);
+                        breakArrows.Hide(fade_duration);
+                    }
                 }
             }
         }
 
-        private void showBreak()
-        {
-            if (letterboxing)
-                letterboxOverlay.Show();
-
-            remainingTimeCounter.Show();
-            info.Show();
-            arrowsOverlay.Show();
-        }
-
-        private void hideBreak()
-        {
-            if (letterboxing)
-                letterboxOverlay.Hide();
-
-            remainingTimeCounter.Hide();
-            info.Hide();
-            arrowsOverlay.Hide();
-        }
-
         private void bindProcessor(ScoreProcessor processor)
         {
             info.AccuracyDisplay.Current.BindTo(processor.Accuracy);
diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs
index 7a0c723ab5..84f6cd606a 100644
--- a/osu.Game/Screens/Play/Player.cs
+++ b/osu.Game/Screens/Play/Player.cs
@@ -26,7 +26,6 @@ using osu.Game.Rulesets.Mods;
 using osu.Game.Rulesets.Scoring;
 using osu.Game.Rulesets.UI;
 using osu.Game.Screens.Backgrounds;
-using osu.Game.Screens.Play.BreaksOverlay;
 using osu.Game.Screens.Ranking;
 using osu.Game.Storyboards.Drawables;
 using OpenTK;
diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj
index ff365ad93e..429e7c7b27 100644
--- a/osu.Game/osu.Game.csproj
+++ b/osu.Game/osu.Game.csproj
@@ -395,14 +395,14 @@
     <Compile Include="Screens\Edit\Screens\Compose\RadioButtons\RadioButtonCollection.cs" />
     <Compile Include="Screens\Edit\Screens\Compose\Timeline\BeatmapWaveformGraph.cs" />
     <Compile Include="Screens\Edit\Screens\Compose\Timeline\TimelineButton.cs" />
-    <Compile Include="Screens\Play\BreaksOverlay\ArrowsOverlay.cs" />
-    <Compile Include="Screens\Play\BreaksOverlay\BlurredIcon.cs" />
-    <Compile Include="Screens\Play\BreaksOverlay\BreakOverlay.cs" />
-    <Compile Include="Screens\Play\BreaksOverlay\GlowIcon.cs" />
-    <Compile Include="Screens\Play\BreaksOverlay\InfoContainer.cs" />
-    <Compile Include="Screens\Play\BreaksOverlay\InfoLine.cs" />
-    <Compile Include="Screens\Play\BreaksOverlay\LetterboxOverlay.cs" />
-    <Compile Include="Screens\Play\BreaksOverlay\RemainingTimeCounter.cs" />
+    <Compile Include="Screens\Play\Break\BreakArrows.cs" />
+    <Compile Include="Screens\Play\Break\BlurredIcon.cs" />
+    <Compile Include="Screens\Play\BreakOverlay.cs" />
+    <Compile Include="Screens\Play\Break\GlowIcon.cs" />
+    <Compile Include="Screens\Play\Break\BreakInfo.cs" />
+    <Compile Include="Screens\Play\Break\BreakInfoLine.cs" />
+    <Compile Include="Screens\Play\Break\LetterboxOverlay.cs" />
+    <Compile Include="Screens\Play\Break\RemainingTimeCounter.cs" />
     <Compile Include="Beatmaps\Legacy\LegacyBeatmap.cs" />
     <Compile Include="Beatmaps\RankStatus.cs" />
     <Compile Include="Beatmaps\Timing\BreakPeriod.cs" />

From b62ed004f274b3bf25704d2d8dfb6ebe909a715a Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Wed, 7 Mar 2018 19:14:42 +0900
Subject: [PATCH 04/16] Remove judgements when deciding a new judgement for a
 HitObject

Generally happens when rewinding.
---
 .../UI/DrawableManiaJudgement.cs              |  5 ++--
 osu.Game.Rulesets.Mania/UI/ManiaStage.cs      |  7 +++---
 .../Objects/Drawables/DrawableOsuJudgement.cs |  6 ++---
 osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs      | 11 ++++-----
 .../UI/DrawableTaikoJudgement.cs              |  7 ++----
 osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs  |  7 +++---
 .../Rulesets/Judgements/DrawableJudgement.cs  |  6 ++++-
 osu.Game/Rulesets/UI/JudgementContainer.cs    | 24 +++++++++++++++++++
 osu.Game/osu.Game.csproj                      |  1 +
 9 files changed, 50 insertions(+), 24 deletions(-)
 create mode 100644 osu.Game/Rulesets/UI/JudgementContainer.cs

diff --git a/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs b/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs
index 8a03f5a785..b8ae09c4a0 100644
--- a/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs
+++ b/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs
@@ -3,13 +3,14 @@
 
 using osu.Framework.Graphics;
 using osu.Game.Rulesets.Judgements;
+using osu.Game.Rulesets.Objects.Drawables;
 
 namespace osu.Game.Rulesets.Mania.UI
 {
     internal class DrawableManiaJudgement : DrawableJudgement
     {
-        public DrawableManiaJudgement(Judgement judgement)
-            : base(judgement)
+        public DrawableManiaJudgement(Judgement judgement, DrawableHitObject judgedObject)
+            : base(judgement, judgedObject)
         {
             JudgementText.TextSize = 25;
         }
diff --git a/osu.Game.Rulesets.Mania/UI/ManiaStage.cs b/osu.Game.Rulesets.Mania/UI/ManiaStage.cs
index 2b8039f5df..d4ca704829 100644
--- a/osu.Game.Rulesets.Mania/UI/ManiaStage.cs
+++ b/osu.Game.Rulesets.Mania/UI/ManiaStage.cs
@@ -15,6 +15,7 @@ using osu.Game.Rulesets.Mania.Beatmaps;
 using osu.Game.Rulesets.Mania.Objects;
 using osu.Game.Rulesets.Mania.Objects.Drawables;
 using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.UI;
 using osu.Game.Rulesets.UI.Scrolling;
 using OpenTK;
 using OpenTK.Graphics;
@@ -40,7 +41,7 @@ namespace osu.Game.Rulesets.Mania.UI
         private readonly Container<Drawable> content;
 
         public Container<DrawableManiaJudgement> Judgements => judgements;
-        private readonly Container<DrawableManiaJudgement> judgements;
+        private readonly JudgementContainer<DrawableManiaJudgement> judgements;
 
         private readonly Container topLevelContainer;
 
@@ -114,7 +115,7 @@ namespace osu.Game.Rulesets.Mania.UI
                                 Padding = new MarginPadding { Top = HIT_TARGET_POSITION }
                             }
                         },
-                        judgements = new Container<DrawableManiaJudgement>
+                        judgements = new JudgementContainer<DrawableManiaJudgement>
                         {
                             Anchor = Anchor.TopCentre,
                             Origin = Anchor.Centre,
@@ -171,7 +172,7 @@ namespace osu.Game.Rulesets.Mania.UI
         internal void OnJudgement(DrawableHitObject judgedObject, Judgement judgement)
         {
             judgements.Clear();
-            judgements.Add(new DrawableManiaJudgement(judgement)
+            judgements.Add(new DrawableManiaJudgement(judgement, judgedObject)
             {
                 Anchor = Anchor.Centre,
                 Origin = Anchor.Centre,
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs
index 716f4b629b..0b1df4bdf5 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs
@@ -2,17 +2,17 @@
 // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
 
 using osu.Framework.Graphics;
-using osu.Game.Rulesets.Osu.Judgements;
 using OpenTK;
 using osu.Game.Rulesets.Judgements;
+using osu.Game.Rulesets.Objects.Drawables;
 using osu.Game.Rulesets.Scoring;
 
 namespace osu.Game.Rulesets.Osu.Objects.Drawables
 {
     public class DrawableOsuJudgement : DrawableJudgement
     {
-        public DrawableOsuJudgement(OsuJudgement judgement)
-            : base(judgement)
+        public DrawableOsuJudgement(Judgement judgement, DrawableHitObject judgedObject)
+            : base(judgement, judgedObject)
         {
         }
 
diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs
index 7f8cbce78e..98a8096678 100644
--- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs
+++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs
@@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Osu.UI
     public class OsuPlayfield : Playfield
     {
         private readonly Container approachCircles;
-        private readonly Container judgementLayer;
+        private readonly JudgementContainer<DrawableOsuJudgement> judgementLayer;
         private readonly ConnectionRenderer<OsuHitObject> connectionLayer;
 
         // Todo: This should not be a thing, but is currently required for the editor
@@ -40,7 +40,7 @@ namespace osu.Game.Rulesets.Osu.UI
                     RelativeSizeAxes = Axes.Both,
                     Depth = 2,
                 },
-                judgementLayer = new Container
+                judgementLayer = new JudgementContainer<DrawableOsuJudgement>
                 {
                     RelativeSizeAxes = Axes.Both,
                     Depth = 1,
@@ -75,16 +75,13 @@ namespace osu.Game.Rulesets.Osu.UI
 
         private void onJudgement(DrawableHitObject judgedObject, Judgement judgement)
         {
-            var osuJudgement = (OsuJudgement)judgement;
-            var osuObject = (OsuHitObject)judgedObject.HitObject;
-
             if (!judgedObject.DisplayJudgement)
                 return;
 
-            DrawableOsuJudgement explosion = new DrawableOsuJudgement(osuJudgement)
+            DrawableOsuJudgement explosion = new DrawableOsuJudgement(judgement, judgedObject)
             {
                 Origin = Anchor.Centre,
-                Position = osuObject.StackedEndPosition + osuJudgement.PositionOffset
+                Position = ((OsuHitObject)judgedObject.HitObject).StackedEndPosition + ((OsuJudgement)judgement).PositionOffset
             };
 
             judgementLayer.Add(explosion);
diff --git a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoJudgement.cs b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoJudgement.cs
index c0e8bd1b5a..6274232ffd 100644
--- a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoJudgement.cs
+++ b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoJudgement.cs
@@ -15,17 +15,14 @@ namespace osu.Game.Rulesets.Taiko.UI
     /// </summary>
     public class DrawableTaikoJudgement : DrawableJudgement
     {
-        public readonly DrawableHitObject JudgedObject;
-
         /// <summary>
         /// Creates a new judgement text.
         /// </summary>
         /// <param name="judgedObject">The object which is being judged.</param>
         /// <param name="judgement">The judgement to visualise.</param>
-        public DrawableTaikoJudgement(DrawableHitObject judgedObject, Judgement judgement)
-            : base(judgement)
+        public DrawableTaikoJudgement(Judgement judgement, DrawableHitObject judgedObject)
+            : base(judgement, judgedObject)
         {
-            JudgedObject = judgedObject;
         }
 
         [BackgroundDependencyLoader]
diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs
index 49c87f7480..75aaceaecb 100644
--- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs
+++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs
@@ -16,6 +16,7 @@ using System.Linq;
 using osu.Game.Rulesets.Judgements;
 using osu.Game.Rulesets.Taiko.Objects.Drawables;
 using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Rulesets.UI;
 using osu.Game.Rulesets.UI.Scrolling;
 
 namespace osu.Game.Rulesets.Taiko.UI
@@ -41,7 +42,7 @@ namespace osu.Game.Rulesets.Taiko.UI
 
         private readonly Container<HitExplosion> hitExplosionContainer;
         private readonly Container<KiaiHitExplosion> kiaiExplosionContainer;
-        private readonly Container<DrawableTaikoJudgement> judgementContainer;
+        private readonly JudgementContainer<DrawableTaikoJudgement> judgementContainer;
 
         protected override Container<Drawable> Content => content;
         private readonly Container content;
@@ -131,7 +132,7 @@ namespace osu.Game.Rulesets.Taiko.UI
                             Margin = new MarginPadding { Left = HIT_TARGET_OFFSET },
                             Blending = BlendingMode.Additive
                         },
-                        judgementContainer = new Container<DrawableTaikoJudgement>
+                        judgementContainer = new JudgementContainer<DrawableTaikoJudgement>
                         {
                             Name = "Judgements",
                             RelativeSizeAxes = Axes.Y,
@@ -227,7 +228,7 @@ namespace osu.Game.Rulesets.Taiko.UI
         {
             if (judgedObject.DisplayJudgement && judgementContainer.FirstOrDefault(j => j.JudgedObject == judgedObject) == null)
             {
-                judgementContainer.Add(new DrawableTaikoJudgement(judgedObject, judgement)
+                judgementContainer.Add(new DrawableTaikoJudgement(judgement, judgedObject)
                 {
                     Anchor = judgement.IsHit ? Anchor.TopLeft : Anchor.CentreLeft,
                     Origin = judgement.IsHit ? Anchor.BottomCentre : Anchor.Centre,
diff --git a/osu.Game/Rulesets/Judgements/DrawableJudgement.cs b/osu.Game/Rulesets/Judgements/DrawableJudgement.cs
index c1bf55b214..a3811654f2 100644
--- a/osu.Game/Rulesets/Judgements/DrawableJudgement.cs
+++ b/osu.Game/Rulesets/Judgements/DrawableJudgement.cs
@@ -9,6 +9,7 @@ using osu.Framework.Graphics.Containers;
 using osu.Framework.Graphics.Sprites;
 using osu.Game.Graphics;
 using osu.Game.Graphics.Sprites;
+using osu.Game.Rulesets.Objects.Drawables;
 using osu.Game.Rulesets.Scoring;
 
 namespace osu.Game.Rulesets.Judgements
@@ -20,15 +21,18 @@ namespace osu.Game.Rulesets.Judgements
     {
         protected readonly Judgement Judgement;
 
+        public readonly DrawableHitObject JudgedObject;
+
         protected readonly SpriteText JudgementText;
 
         /// <summary>
         /// Creates a drawable which visualises a <see cref="Judgements.Judgement"/>.
         /// </summary>
         /// <param name="judgement">The judgement to visualise.</param>
-        public DrawableJudgement(Judgement judgement)
+        public DrawableJudgement(Judgement judgement, DrawableHitObject judgedObject)
         {
             Judgement = judgement;
+            JudgedObject = judgedObject;
 
             AutoSizeAxes = Axes.Both;
 
diff --git a/osu.Game/Rulesets/UI/JudgementContainer.cs b/osu.Game/Rulesets/UI/JudgementContainer.cs
new file mode 100644
index 0000000000..1291b9fc98
--- /dev/null
+++ b/osu.Game/Rulesets/UI/JudgementContainer.cs
@@ -0,0 +1,24 @@
+// 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;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Rulesets.Judgements;
+
+namespace osu.Game.Rulesets.UI
+{
+    public class JudgementContainer<T> : Container<T>
+        where T : DrawableJudgement
+    {
+        public override void Add(T judgement)
+        {
+            if (judgement == null) throw new ArgumentNullException(nameof(judgement));
+
+            // remove any existing judgements for the judged object.
+            // this can be the case when rewinding.
+            RemoveAll(c => c.JudgedObject == judgement.JudgedObject);
+
+            base.Add(judgement);
+        }
+    }
+}
diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj
index b8ada7c017..2a72023e74 100644
--- a/osu.Game/osu.Game.csproj
+++ b/osu.Game/osu.Game.csproj
@@ -374,6 +374,7 @@
     <Compile Include="Rulesets\Replays\ReplayFrame.cs" />
     <Compile Include="Rulesets\Replays\Types\IConvertibleReplayFrame.cs" />
     <Compile Include="Rulesets\Scoring\Legacy\LegacyScoreParser.cs" />
+    <Compile Include="Rulesets\UI\JudgementContainer.cs" />
     <Compile Include="Rulesets\UI\ScalableContainer.cs" />
     <Compile Include="Screens\Play\PlayerSettings\VisualSettings.cs" />
     <Compile Include="Rulesets\Objects\CatmullApproximator.cs" />

From ee96e974a89b81d22d12ce86560d4bf02097819d Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Thu, 8 Mar 2018 09:43:31 +0900
Subject: [PATCH 05/16] Update framework

---
 osu-framework | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/osu-framework b/osu-framework
index 6372fb22c1..865b0df18b 160000
--- a/osu-framework
+++ b/osu-framework
@@ -1 +1 @@
-Subproject commit 6372fb22c1c85f600921a139849b8dedf71026d5
+Subproject commit 865b0df18bb240190cdf7a7f60d44c0b28c84c5f

From 3b5699911808e7796de3dc40d9d556e097d4e665 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Thu, 8 Mar 2018 12:57:12 +0900
Subject: [PATCH 06/16] Add drawable to display (and update) relative dates

---
 osu.Game/Graphics/DrawableDate.cs             | 64 +++++++++++++++++++
 osu.Game/Overlays/Profile/ProfileHeader.cs    | 59 ++++++++---------
 .../Sections/Ranks/DrawableProfileScore.cs    |  7 +-
 .../Sections/Recent/DrawableRecentActivity.cs |  9 +--
 osu.Game/osu.Game.csproj                      |  1 +
 5 files changed, 95 insertions(+), 45 deletions(-)
 create mode 100644 osu.Game/Graphics/DrawableDate.cs

diff --git a/osu.Game/Graphics/DrawableDate.cs b/osu.Game/Graphics/DrawableDate.cs
new file mode 100644
index 0000000000..452443f9d0
--- /dev/null
+++ b/osu.Game/Graphics/DrawableDate.cs
@@ -0,0 +1,64 @@
+// 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;
+using Humanizer;
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Cursor;
+using osu.Framework.Threading;
+using osu.Game.Graphics.Sprites;
+
+namespace osu.Game.Graphics
+{
+    public class DrawableDate : OsuSpriteText, IHasTooltip
+    {
+        private readonly DateTimeOffset date;
+        private ScheduledDelegate updateTask;
+
+        public DrawableDate(DateTimeOffset date)
+        {
+            AutoSizeAxes = Axes.Both;
+            Font = "Exo2.0-RegularItalic";
+
+            this.date = date.ToLocalTime();
+        }
+
+        [BackgroundDependencyLoader]
+        private void load()
+        {
+            updateTime();
+        }
+
+        protected override void LoadComplete()
+        {
+            base.LoadComplete();
+            Scheduler.Add(updateTimeWithReschedule);
+        }
+
+        private void updateTimeWithReschedule()
+        {
+            updateTime();
+
+            var diffToNow = DateTimeOffset.Now.Subtract(date);
+
+            double timeUntilNextUpdate = 1000;
+            if (diffToNow.TotalSeconds > 60)
+            {
+                timeUntilNextUpdate *= 60;
+                if (diffToNow.TotalMinutes > 60)
+                {
+                    timeUntilNextUpdate *= 60;
+
+                    if (diffToNow.TotalHours > 24)
+                        timeUntilNextUpdate *= 24;
+                }
+            }
+
+            Scheduler.AddDelayed(updateTimeWithReschedule, timeUntilNextUpdate);
+        }
+
+        private void updateTime() => Text = date.Humanize();
+        public string TooltipText => date.ToString();
+    }
+}
diff --git a/osu.Game/Overlays/Profile/ProfileHeader.cs b/osu.Game/Overlays/Profile/ProfileHeader.cs
index d085800f41..f4b363cd91 100644
--- a/osu.Game/Overlays/Profile/ProfileHeader.cs
+++ b/osu.Game/Overlays/Profile/ProfileHeader.cs
@@ -130,11 +130,7 @@ namespace osu.Game.Overlays.Profile
                         }
                     }
                 },
-                infoTextLeft = new OsuTextFlowContainer(t =>
-                {
-                    t.TextSize = 14;
-                    t.Alpha = 0.8f;
-                })
+                infoTextLeft = new OsuTextFlowContainer(t => t.TextSize = 14)
                 {
                     X = UserProfileOverlay.CONTENT_X_MARGIN,
                     Y = cover_height + 20,
@@ -318,11 +314,23 @@ namespace osu.Game.Overlays.Profile
                 colourBar.Show();
             }
 
-            void boldItalic(SpriteText t)
+            void boldItalic(SpriteText t) => t.Font = @"Exo2.0-BoldItalic";
+            void lightText(SpriteText t) => t.Alpha = 0.8f;
+
+            OsuSpriteText createScoreText(string text) => new OsuSpriteText
             {
-                t.Font = @"Exo2.0-BoldItalic";
-                t.Alpha = 1;
-            }
+                TextSize = 14,
+                Text = text
+            };
+
+            OsuSpriteText createScoreNumberText(string text) => new OsuSpriteText
+            {
+                TextSize = 14,
+                Font = @"Exo2.0-Bold",
+                Anchor = Anchor.TopRight,
+                Origin = Anchor.TopRight,
+                Text = text
+            };
 
             if (user.Age != null)
             {
@@ -331,7 +339,7 @@ namespace osu.Game.Overlays.Profile
 
             if (user.Country != null)
             {
-                infoTextLeft.AddText("from ");
+                infoTextLeft.AddText("from ", lightText);
                 infoTextLeft.AddText(user.Country.FullName, boldItalic);
                 countryFlag.Country = user.Country;
             }
@@ -344,18 +352,18 @@ namespace osu.Game.Overlays.Profile
             }
             else
             {
-                infoTextLeft.AddText("Joined ");
-                infoTextLeft.AddText(user.JoinDate.LocalDateTime.ToShortDateString(), boldItalic);
+                infoTextLeft.AddText("Joined ", lightText);
+                infoTextLeft.AddText(new DrawableDate(user.JoinDate), boldItalic);
             }
 
             infoTextLeft.NewLine();
-            infoTextLeft.AddText("Last seen ");
-            infoTextLeft.AddText(user.LastVisit.LocalDateTime.ToShortDateString(), boldItalic);
+            infoTextLeft.AddText("Last seen ", lightText);
+            infoTextLeft.AddText(new DrawableDate(user.LastVisit), boldItalic);
             infoTextLeft.NewParagraph();
 
             if (user.PlayStyle?.Length > 0)
             {
-                infoTextLeft.AddText("Plays with ");
+                infoTextLeft.AddText("Plays with ", lightText);
                 infoTextLeft.AddText(string.Join(", ", user.PlayStyle), boldItalic);
             }
 
@@ -411,23 +419,6 @@ namespace osu.Game.Overlays.Profile
             }
         }
 
-        // These could be local functions when C# 7 enabled
-
-        private OsuSpriteText createScoreText(string text) => new OsuSpriteText
-        {
-            TextSize = 14,
-            Text = text
-        };
-
-        private OsuSpriteText createScoreNumberText(string text) => new OsuSpriteText
-        {
-            TextSize = 14,
-            Font = @"Exo2.0-Bold",
-            Anchor = Anchor.TopRight,
-            Origin = Anchor.TopRight,
-            Text = text
-        };
-
         private void tryAddInfoRightLine(FontAwesome icon, string str, string url = null)
         {
             if (string.IsNullOrEmpty(str)) return;
@@ -436,10 +427,12 @@ namespace osu.Game.Overlays.Profile
             if (url != null)
             {
                 infoTextRight.AddLink(" " + str, url);
-            } else
+            }
+            else
             {
                 infoTextRight.AddText(" " + str);
             }
+
             infoTextRight.NewLine();
         }
 
diff --git a/osu.Game/Overlays/Profile/Sections/Ranks/DrawableProfileScore.cs b/osu.Game/Overlays/Profile/Sections/Ranks/DrawableProfileScore.cs
index bb1a409f2e..509356ae04 100644
--- a/osu.Game/Overlays/Profile/Sections/Ranks/DrawableProfileScore.cs
+++ b/osu.Game/Overlays/Profile/Sections/Ranks/DrawableProfileScore.cs
@@ -54,12 +54,7 @@ namespace osu.Game.Overlays.Profile.Sections.Ranks
             RightFlowContainer.SetLayoutPosition(text, 1);
 
             LeftFlowContainer.Add(new BeatmapMetadataContainer(Score.Beatmap));
-            LeftFlowContainer.Add(new OsuSpriteText
-            {
-                Text = Score.Date.LocalDateTime.ToShortDateString(),
-                TextSize = 11,
-                Colour = OsuColour.Gray(0xAA),
-            });
+            LeftFlowContainer.Add(new DrawableDate(Score.Date));
 
             foreach (Mod mod in Score.Mods)
                 modsContainer.Add(new ModIcon(mod) { Scale = new Vector2(0.5f) });
diff --git a/osu.Game/Overlays/Profile/Sections/Recent/DrawableRecentActivity.cs b/osu.Game/Overlays/Profile/Sections/Recent/DrawableRecentActivity.cs
index 2dde8a3d54..e8be8d1e44 100644
--- a/osu.Game/Overlays/Profile/Sections/Recent/DrawableRecentActivity.cs
+++ b/osu.Game/Overlays/Profile/Sections/Recent/DrawableRecentActivity.cs
@@ -6,7 +6,6 @@ using osu.Framework.Graphics;
 using osu.Framework.Graphics.Containers;
 using osu.Game.Graphics;
 using osu.Game.Graphics.Containers;
-using osu.Game.Graphics.Sprites;
 using osu.Game.Online.API;
 using osu.Game.Online.API.Requests;
 using osu.Game.Online.Chat;
@@ -40,14 +39,12 @@ namespace osu.Game.Overlays.Profile.Sections.Recent
                 RelativeSizeAxes = Axes.X,
             });
 
-            RightFlowContainer.Add(new OsuSpriteText
+            RightFlowContainer.Add(new DrawableDate(activity.CreatedAt)
             {
-                Text = activity.CreatedAt.LocalDateTime.ToShortDateString(),
+                TextSize =  13,
+                Colour = OsuColour.Gray(0xAA),
                 Anchor = Anchor.TopRight,
                 Origin = Anchor.TopRight,
-                Font = "Exo2.0-RegularItalic",
-                TextSize = 12,
-                Colour = OsuColour.Gray(0xAA),
             });
 
             var formatted = createMessage();
diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj
index 6f7c92ab5a..01074318cd 100644
--- a/osu.Game/osu.Game.csproj
+++ b/osu.Game/osu.Game.csproj
@@ -287,6 +287,7 @@
     <Compile Include="Database\MutableDatabaseBackedStore.cs" />
     <Compile Include="Database\SingletonContextFactory.cs" />
     <Compile Include="Graphics\Containers\LinkFlowContainer.cs" />
+    <Compile Include="Graphics\DrawableDate.cs" />
     <Compile Include="Graphics\Textures\LargeTextureStore.cs" />
     <Compile Include="IO\Archives\ArchiveReader.cs" />
     <Compile Include="IO\Archives\LegacyFilesystemReader.cs" />

From 6ad962fc8b7ec6e7390be778012d17988cb81777 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Fri, 9 Mar 2018 14:34:05 +0900
Subject: [PATCH 07/16] Interpolate ParallaxContainer's scale

Things were a bit jumpy when a screen was adjusting `ParallaxAmount`. This smoothes the applied scale changes to look great again.

Most noticeable when hitting the retry hotkey (`~`) from gameplay.
---
 osu.Game/Graphics/Containers/ParallaxContainer.cs | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/osu.Game/Graphics/Containers/ParallaxContainer.cs b/osu.Game/Graphics/Containers/ParallaxContainer.cs
index febe52d775..97d6225534 100644
--- a/osu.Game/Graphics/Containers/ParallaxContainer.cs
+++ b/osu.Game/Graphics/Containers/ParallaxContainer.cs
@@ -66,8 +66,10 @@ namespace osu.Game.Graphics.Containers
             {
                 Vector2 offset = (input.CurrentState.Mouse == null ? Vector2.Zero : ToLocalSpace(input.CurrentState.Mouse.NativeState.Position) - DrawSize / 2) * ParallaxAmount;
 
-                content.Position = Interpolation.ValueAt(MathHelper.Clamp(Clock.ElapsedFrameTime, 0, 1000), content.Position, offset, 0, 1000, Easing.OutQuint);
-                content.Scale = new Vector2(1 + ParallaxAmount);
+                double elapsed = MathHelper.Clamp(Clock.ElapsedFrameTime, 0, 1000);
+
+                content.Position = Interpolation.ValueAt(elapsed, content.Position, offset, 0, 1000, Easing.OutQuint);
+                content.Scale = Interpolation.ValueAt(elapsed, content.Scale, new Vector2(1 + ParallaxAmount), 0, 1000, Easing.OutQuint);
             }
 
             firstUpdate = false;

From 25fb527cc7bd7f1d1c3a855db9ca2281ee3175a3 Mon Sep 17 00:00:00 2001
From: naoey <naoey@protonmail.ch>
Date: Fri, 9 Mar 2018 15:51:00 +0530
Subject: [PATCH 08/16] Remove previous fix and move filtered logic to
 carousel.

- Add an optional bool parameter to SelectBeatmap to skip selecting
filtered maps
---
 osu.Game/Screens/Select/BeatmapCarousel.cs | 31 +++++++++++++--------
 osu.Game/Screens/Select/SongSelect.cs      | 32 ----------------------
 2 files changed, 20 insertions(+), 43 deletions(-)

diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs
index 9793440348..02bad82ca9 100644
--- a/osu.Game/Screens/Select/BeatmapCarousel.cs
+++ b/osu.Game/Screens/Select/BeatmapCarousel.cs
@@ -169,20 +169,29 @@ namespace osu.Game.Screens.Select
             });
         }
 
-        public void SelectBeatmap(BeatmapInfo beatmap)
+        /// <summary>
+        /// Selects a given beatmap on the carousel.
+        /// </summary>
+        /// <param name="beatmap">The beatmap to select.</param>
+        /// <param name="skipFiltered">Whether to skip selecting filtered beatmaps.</param>
+        /// <returns>True if a selection was made, false if it was skipped.</returns>
+        public bool SelectBeatmap(BeatmapInfo beatmap, bool skipFiltered = false)
         {
             if (beatmap?.Hidden != false)
-                return;
+                return false;
 
-            foreach (CarouselBeatmapSet group in beatmapSets)
-            {
-                var item = group.Beatmaps.FirstOrDefault(p => p.Beatmap.Equals(beatmap));
-                if (item != null)
-                {
-                    select(item);
-                    return;
-                }
-            }
+            var group = beatmapSets.FirstOrDefault(s => s.BeatmapSet.OnlineBeatmapSetID == beatmap.BeatmapSet.OnlineBeatmapSetID);
+
+            if (group == null || !skipFiltered && group.Filtered)
+                return false;
+
+            var item = group.Beatmaps.FirstOrDefault(p => p.Beatmap.Equals(beatmap));
+
+            if (item == null || !skipFiltered && item.Filtered)
+                return false;
+
+            select(item);
+            return true;
         }
 
         /// <summary>
diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs
index d4fd64dcd9..2c8dcae3cf 100644
--- a/osu.Game/Screens/Select/SongSelect.cs
+++ b/osu.Game/Screens/Select/SongSelect.cs
@@ -267,10 +267,7 @@ namespace osu.Game.Screens.Select
         protected void WorkingBeatmapChanged(WorkingBeatmap beatmap)
         {
             if (IsCurrentScreen)
-            {
                 Carousel.SelectBeatmap(beatmap?.BeatmapInfo);
-                ensurePlayableRuleset();
-            }
         }
 
         /// <summary>
@@ -328,7 +325,6 @@ namespace osu.Game.Screens.Select
         {
             base.OnEntering(last);
 
-            ensurePlayableRuleset();
             Content.FadeInFromZero(250);
             FilterControl.Activate();
         }
@@ -456,34 +452,6 @@ namespace osu.Game.Screens.Select
             }
         }
 
-        private void ensurePlayableRuleset()
-        {
-            if (Beatmap.IsDefault)
-                // DummyBeatmap won't be playable anyway
-                return;
-
-            bool conversionAllowed = rulesetConversionAllowed.Value;
-            int? currentRuleset = Ruleset.Value.ID;
-            int beatmapRuleset = Beatmap.Value.BeatmapInfo.RulesetID;
-
-            if (currentRuleset == beatmapRuleset || conversionAllowed && beatmapRuleset == 0)
-                // Current beatmap is playable, nothing more to do
-                return;
-
-            // Otherwise, first check if the current beatmapset has any playable beatmaps
-            BeatmapInfo beatmap = Beatmap.Value.BeatmapSetInfo.Beatmaps?.FirstOrDefault(b => b.RulesetID == currentRuleset || conversionAllowed && b.RulesetID == 0);
-
-            // If it does then update the WorkingBeatmap
-            if (beatmap != null)
-            {
-                Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmap);
-                return;
-            }
-
-            // If it doesn't, then update the current ruleset so that the current beatmap is playable
-            Ruleset.Value = Beatmap.Value.BeatmapInfo.Ruleset;
-        }
-
         private void onBeatmapSetAdded(BeatmapSetInfo s) => Carousel.UpdateBeatmapSet(s);
         private void onBeatmapSetRemoved(BeatmapSetInfo s) => Carousel.RemoveBeatmapSet(s);
         private void onBeatmapRestored(BeatmapInfo b) => Carousel.UpdateBeatmapSet(beatmaps.QueryBeatmapSet(s => s.ID == b.BeatmapSetInfoID));

From d04f47718fa73622a6610897357dd3c9de8acdfb Mon Sep 17 00:00:00 2001
From: naoey <naoey@protonmail.ch>
Date: Fri, 9 Mar 2018 16:22:59 +0530
Subject: [PATCH 09/16] Make song select choose random when initial selection
 fails.

- Revert TestCasePlaySongSelect to master
---
 .../Visual/TestCasePlaySongSelect.cs          | 100 ++----------------
 osu.Game/Screens/Select/BeatmapCarousel.cs    |  32 ++++--
 osu.Game/Screens/Select/SongSelect.cs         |  25 ++---
 3 files changed, 37 insertions(+), 120 deletions(-)

diff --git a/osu.Game.Tests/Visual/TestCasePlaySongSelect.cs b/osu.Game.Tests/Visual/TestCasePlaySongSelect.cs
index 8532962389..cede0160bc 100644
--- a/osu.Game.Tests/Visual/TestCasePlaySongSelect.cs
+++ b/osu.Game.Tests/Visual/TestCasePlaySongSelect.cs
@@ -11,7 +11,6 @@ using osu.Framework.Allocation;
 using osu.Framework.Extensions;
 using osu.Framework.MathUtils;
 using osu.Game.Beatmaps;
-using osu.Game.Configuration;
 using osu.Game.Database;
 using osu.Game.Rulesets;
 using osu.Game.Screens.Select;
@@ -56,22 +55,10 @@ namespace osu.Game.Tests.Visual
             public WorkingBeatmap CurrentBeatmap => Beatmap.Value;
             public WorkingBeatmap CurrentBeatmapDetailsBeatmap => BeatmapDetails.Beatmap;
             public new BeatmapCarousel Carousel => base.Carousel;
-
-            public void SetRuleset(RulesetInfo ruleset) => Ruleset.Value = ruleset;
-
-            public int? RulesetID => Ruleset.Value.ID;
-
-            protected override void Dispose(bool isDisposing)
-            {
-                base.Dispose(isDisposing);
-
-                // Necessary while running tests because gc is moody and uncollected object interferes with OnEntering test
-                Beatmap.ValueChanged -= WorkingBeatmapChanged;
-            }
         }
 
         [BackgroundDependencyLoader]
-        private void load(OsuGameBase game, OsuConfigManager config)
+        private void load(OsuGameBase game)
         {
             TestSongSelect songSelect = null;
 
@@ -90,7 +77,6 @@ namespace osu.Game.Tests.Visual
             {
                 if (deleteMaps)
                 {
-                    // TODO: check why this alone doesn't allow import test to run twice in the same session, probably because the delete op is not saved?
                     manager.Delete(manager.GetAllUsableBeatmapSets());
                     game.Beatmap.SetDefault();
                 }
@@ -102,8 +88,6 @@ namespace osu.Game.Tests.Visual
                 }
 
                 Add(songSelect = new TestSongSelect());
-
-                songSelect?.SetRuleset(rulesets.AvailableRulesets.First());
             });
 
             loadNewSongSelect(true);
@@ -118,36 +102,6 @@ namespace osu.Game.Tests.Visual
             {
                 for (int i = 0; i < 100; i += 10)
                     manager.Import(createTestBeatmapSet(i));
-
-                // also import a set which has a single non - osu ruleset beatmap
-                manager.Import(new BeatmapSetInfo
-                {
-                    OnlineBeatmapSetID = 1993,
-                    Hash = new MemoryStream(Encoding.UTF8.GetBytes(Guid.NewGuid().ToString())).ComputeMD5Hash(),
-                    Metadata = new BeatmapMetadata
-                    {
-                        OnlineBeatmapSetID = 1993,
-                        // Create random metadata, then we can check if sorting works based on these
-                        Artist = "MONACA " + RNG.Next(0, 9),
-                        Title = "Black Song " + RNG.Next(0, 9),
-                        AuthorString = "Some Guy " + RNG.Next(0, 9),
-                    },
-                    Beatmaps = new List<BeatmapInfo>
-                    {
-                        new BeatmapInfo
-                        {
-                            OnlineBeatmapID = 1994,
-                            Ruleset = rulesets.AvailableRulesets.ElementAt(3),
-                            RulesetID = 3,
-                            Path = "normal.fruits",
-                            Version = "Normal",
-                            BaseDifficulty = new BeatmapDifficulty
-                            {
-                                OverallDifficulty = 3.5f,
-                            }
-                        },
-                    }
-                });
             });
 
             AddWaitStep(3);
@@ -161,45 +115,6 @@ namespace osu.Game.Tests.Visual
             AddStep(@"Sort by Title", delegate { songSelect.FilterControl.Sort = SortMode.Title; });
             AddStep(@"Sort by Author", delegate { songSelect.FilterControl.Sort = SortMode.Author; });
             AddStep(@"Sort by Difficulty", delegate { songSelect.FilterControl.Sort = SortMode.Difficulty; });
-
-            // Test that song select sets a playable beatmap while entering
-            AddStep(@"Remove song select", () =>
-            {
-                Remove(songSelect);
-                songSelect.Dispose();
-                songSelect = null;
-            });
-            AddStep(@"Set non-osu beatmap", () => game.Beatmap.Value = manager.GetWorkingBeatmap(manager.GetAllUsableBeatmapSets().First().Beatmaps.First(b => b.RulesetID != 0)));
-            AddAssert(@"Non-osu beatmap set", () => game.Beatmap.Value.BeatmapInfo.RulesetID != 0);
-            loadNewSongSelect();
-            AddWaitStep(3);
-            AddAssert(@"osu beatmap set", () => game.Beatmap.Value.BeatmapInfo.RulesetID == 0);
-
-            // Test that song select changes WorkingBeatmap to be playable in current ruleset when updated externally
-            AddStep(@"Try set non-osu beatmap", () =>
-            {
-                var testMap = manager.GetAllUsableBeatmapSets().First().Beatmaps.First(b => b.RulesetID != 0);
-                songSelect.SetRuleset(rulesets.AvailableRulesets.First());
-                game.Beatmap.Value = manager.GetWorkingBeatmap(testMap);
-            });
-            AddAssert(@"Beatmap changed to osu", () => songSelect.RulesetID == 0 && game.Beatmap.Value.BeatmapInfo.RulesetID == 0);
-
-            // Test that song select updates WorkingBeatmap when ruleset conversion is disabled
-            AddStep(@"Disable beatmap conversion", () => config.Set(OsuSetting.ShowConvertedBeatmaps, false));
-            AddStep(@"Set osu beatmap taiko rs", () =>
-            {
-                game.Beatmap.Value = manager.GetWorkingBeatmap(manager.GetAllUsableBeatmapSets().First().Beatmaps.First(b => b.RulesetID == 0));
-                songSelect.SetRuleset(rulesets.AvailableRulesets.First(r => r.ID == 1));
-            });
-            AddAssert(@"taiko beatmap set", () => songSelect.RulesetID == 1);
-
-            // Test that song select changes the active ruleset when externally set beatmapset has no playable beatmaps
-            AddStep(@"Set fruits only beatmapset", () =>
-            {
-                songSelect.SetRuleset(rulesets.AvailableRulesets.First());
-                game.Beatmap.Value = manager.GetWorkingBeatmap(manager.QueryBeatmapSet(b => b.OnlineBeatmapSetID == 1993).Beatmaps.First());
-            });
-            AddAssert(@"Ruleset changed to fruits", () => songSelect.RulesetID == game.Beatmap.Value.BeatmapInfo.RulesetID);
         }
 
         private BeatmapSetInfo createTestBeatmapSet(int i)
@@ -221,8 +136,7 @@ namespace osu.Game.Tests.Visual
                     new BeatmapInfo
                     {
                         OnlineBeatmapID = 1234 + i,
-                        Ruleset = rulesets.AvailableRulesets.ElementAt(0),
-                        RulesetID = 0,
+                        Ruleset = rulesets.AvailableRulesets.First(),
                         Path = "normal.osu",
                         Version = "Normal",
                         BaseDifficulty = new BeatmapDifficulty
@@ -233,9 +147,8 @@ namespace osu.Game.Tests.Visual
                     new BeatmapInfo
                     {
                         OnlineBeatmapID = 1235 + i,
-                        Ruleset = rulesets.AvailableRulesets.First(r => r.ID != 0),
-                        RulesetID = 1,
-                        Path = "hard.taiko",
+                        Ruleset = rulesets.AvailableRulesets.First(),
+                        Path = "hard.osu",
                         Version = "Hard",
                         BaseDifficulty = new BeatmapDifficulty
                         {
@@ -245,9 +158,8 @@ namespace osu.Game.Tests.Visual
                     new BeatmapInfo
                     {
                         OnlineBeatmapID = 1236 + i,
-                        Ruleset = rulesets.AvailableRulesets.ElementAt(2),
-                        RulesetID = 2,
-                        Path = "insane.fruits",
+                        Ruleset = rulesets.AvailableRulesets.First(),
+                        Path = "insane.osu",
                         Version = "Insane",
                         BaseDifficulty = new BeatmapDifficulty
                         {
diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs
index 02bad82ca9..287584bf2f 100644
--- a/osu.Game/Screens/Select/BeatmapCarousel.cs
+++ b/osu.Game/Screens/Select/BeatmapCarousel.cs
@@ -171,27 +171,41 @@ namespace osu.Game.Screens.Select
 
         /// <summary>
         /// Selects a given beatmap on the carousel.
+        ///
+        /// If skipFiltered is true, we will try to select another unfiltered beatmap in the same set. If the
+        /// entire set is filtered, no selection is made.
         /// </summary>
         /// <param name="beatmap">The beatmap to select.</param>
         /// <param name="skipFiltered">Whether to skip selecting filtered beatmaps.</param>
-        /// <returns>True if a selection was made, false if it was skipped.</returns>
+        /// <returns>True if a selection was made, False if it wasn't.</returns>
         public bool SelectBeatmap(BeatmapInfo beatmap, bool skipFiltered = false)
         {
             if (beatmap?.Hidden != false)
                 return false;
 
-            var group = beatmapSets.FirstOrDefault(s => s.BeatmapSet.OnlineBeatmapSetID == beatmap.BeatmapSet.OnlineBeatmapSetID);
+            foreach (CarouselBeatmapSet set in beatmapSets)
+            {
+                if (skipFiltered && set.Filtered)
+                    continue;
 
-            if (group == null || !skipFiltered && group.Filtered)
-                return false;
+                var item = set.Beatmaps.FirstOrDefault(p => p.Beatmap.Equals(beatmap));
 
-            var item = group.Beatmaps.FirstOrDefault(p => p.Beatmap.Equals(beatmap));
+                if (item == null)
+                    // The beatmap that needs to be selected doesn't exist in this set
+                    continue;
 
-            if (item == null || !skipFiltered && item.Filtered)
-                return false;
+                if (skipFiltered && item.Filtered)
+                    // The beatmap exists in this set but is filtered, so look for the first unfiltered map in the set
+                    item = set.Beatmaps.FirstOrDefault(b => !b.Filtered);
 
-            select(item);
-            return true;
+                if (item != null)
+                {
+                    select(item);
+                    return true;
+                }
+            }
+
+            return false;
         }
 
         /// <summary>
diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs
index 2c8dcae3cf..461b17338d 100644
--- a/osu.Game/Screens/Select/SongSelect.cs
+++ b/osu.Game/Screens/Select/SongSelect.cs
@@ -2,7 +2,6 @@
 // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
 
 using System;
-using System.Linq;
 using System.Threading;
 using OpenTK;
 using OpenTK.Input;
@@ -10,14 +9,12 @@ using osu.Framework.Allocation;
 using osu.Framework.Audio;
 using osu.Framework.Audio.Sample;
 using osu.Framework.Audio.Track;
-using osu.Framework.Configuration;
 using osu.Framework.Graphics;
 using osu.Framework.Graphics.Containers;
 using osu.Framework.Input;
 using osu.Framework.Screens;
 using osu.Framework.Threading;
 using osu.Game.Beatmaps;
-using osu.Game.Configuration;
 using osu.Game.Graphics;
 using osu.Game.Graphics.Containers;
 using osu.Game.Overlays;
@@ -66,8 +63,6 @@ namespace osu.Game.Screens.Select
         private SampleChannel sampleChangeDifficulty;
         private SampleChannel sampleChangeBeatmap;
 
-        private Bindable<bool> rulesetConversionAllowed;
-
         private CancellationTokenSource initialAddSetsTask;
 
         private DependencyContainer dependencies;
@@ -184,7 +179,7 @@ namespace osu.Game.Screens.Select
         }
 
         [BackgroundDependencyLoader(permitNulls: true)]
-        private void load(BeatmapManager beatmaps, AudioManager audio, DialogOverlay dialog, OsuGame osu, OsuColour colours, OsuConfigManager config)
+        private void load(BeatmapManager beatmaps, AudioManager audio, DialogOverlay dialog, OsuGame osu, OsuColour colours)
         {
             dependencies.CacheAs(this);
 
@@ -199,8 +194,6 @@ namespace osu.Game.Screens.Select
             if (this.beatmaps == null)
                 this.beatmaps = beatmaps;
 
-            rulesetConversionAllowed = config.GetBindable<bool>(OsuSetting.ShowConvertedBeatmaps);
-
             if (osu != null)
                 Ruleset.BindTo(osu.Ruleset);
 
@@ -459,16 +452,14 @@ namespace osu.Game.Screens.Select
 
         private void carouselBeatmapsLoaded()
         {
-            if (!Beatmap.IsDefault && Beatmap.Value.BeatmapSetInfo?.DeletePending == false && Beatmap.Value.BeatmapSetInfo?.Protected == false)
+            if (!Beatmap.IsDefault && Beatmap.Value.BeatmapSetInfo?.DeletePending == false && Beatmap.Value.BeatmapSetInfo?.Protected == false && Carousel.SelectBeatmap(Beatmap.Value.BeatmapInfo, true))
+                return;
+
+            if (Carousel.SelectedBeatmapSet == null && !Carousel.SelectNextRandom())
             {
-                Carousel.SelectBeatmap(Beatmap.Value.BeatmapInfo);
-            }
-            else if (Carousel.SelectedBeatmapSet == null)
-            {
-                if (!Carousel.SelectNextRandom())
-                    // in the case random selection failed, we want to trigger selectionChanged
-                    // to show the dummy beatmap (we have nothing else to display).
-                    carouselSelectionChanged(null);
+                // in the case random selection failed, we want to trigger selectionChanged
+                // to show the dummy beatmap (we have nothing else to display).
+                carouselSelectionChanged(null);
             }
         }
 

From 2c0488b1f17ac169632cc6e575318ddfd9d360e6 Mon Sep 17 00:00:00 2001
From: naoey <naoey20@gmail.com>
Date: Fri, 9 Mar 2018 19:39:28 +0530
Subject: [PATCH 10/16] Invert bool, add test, and handle ruleset change.

---
 .../Visual/TestCaseBeatmapCarousel.cs         | 42 ++++++++++++++++++-
 osu.Game/Screens/Select/BeatmapCarousel.cs    | 10 ++---
 osu.Game/Screens/Select/SongSelect.cs         | 11 +++--
 3 files changed, 54 insertions(+), 9 deletions(-)

diff --git a/osu.Game.Tests/Visual/TestCaseBeatmapCarousel.cs b/osu.Game.Tests/Visual/TestCaseBeatmapCarousel.cs
index fe26366362..c68e548f44 100644
--- a/osu.Game.Tests/Visual/TestCaseBeatmapCarousel.cs
+++ b/osu.Game.Tests/Visual/TestCaseBeatmapCarousel.cs
@@ -12,6 +12,7 @@ using osu.Framework.Extensions;
 using osu.Framework.Graphics;
 using osu.Game.Beatmaps;
 using osu.Game.Configuration;
+using osu.Game.Rulesets;
 using osu.Game.Screens.Select;
 using osu.Game.Screens.Select.Carousel;
 using osu.Game.Screens.Select.Filter;
@@ -22,6 +23,7 @@ namespace osu.Game.Tests.Visual
     public class TestCaseBeatmapCarousel : OsuTestCase
     {
         private TestBeatmapCarousel carousel;
+        private RulesetStore rulesets;
 
         public override IReadOnlyList<Type> RequiredTypes => new[]
         {
@@ -46,8 +48,10 @@ namespace osu.Game.Tests.Visual
         private const int set_count = 5;
 
         [BackgroundDependencyLoader]
-        private void load()
+        private void load(RulesetStore rulesets)
         {
+            this.rulesets = rulesets;
+
             Add(carousel = new TestBeatmapCarousel
             {
                 RelativeSizeAxes = Axes.Both,
@@ -75,6 +79,7 @@ namespace osu.Game.Tests.Visual
             testRemoveAll();
             testEmptyTraversal();
             testHiding();
+            testSelectingFilteredRuleset();
         }
 
         private void ensureRandomFetchSuccess() =>
@@ -363,6 +368,41 @@ namespace osu.Game.Tests.Visual
             }
         }
 
+        private void testSelectingFilteredRuleset()
+        {
+            var testMixed = createTestBeatmapSet(set_count + 1);
+            AddStep("add mixed ruleset beatmapset", () =>
+            {
+                for (int i = 0; i <= 2; i++)
+                {
+                    testMixed.Beatmaps[i].Ruleset = rulesets.AvailableRulesets.ElementAt(i);
+                    testMixed.Beatmaps[i].RulesetID = i;
+                }
+
+                carousel.UpdateBeatmapSet(testMixed);
+            });
+            AddStep("filter to ruleset 0", () =>
+                carousel.Filter(new FilterCriteria { Ruleset = rulesets.AvailableRulesets.ElementAt(0) }, false));
+            AddStep("select filtered map skipping filtered", () => carousel.SelectBeatmap(testMixed.Beatmaps[1], false));
+            AddAssert("unfiltered beatmap selected", () => carousel.SelectedBeatmap.Equals(testMixed.Beatmaps[0]));
+
+            AddStep("remove mixed set", () =>
+            {
+                carousel.RemoveBeatmapSet(testMixed);
+                testMixed = null;
+            });
+            var testSingle = createTestBeatmapSet(set_count + 2);
+            testSingle.Beatmaps.ForEach(b =>
+            {
+                b.Ruleset = rulesets.AvailableRulesets.ElementAt(1);
+                b.RulesetID = b.Ruleset.ID ?? 1;
+            });
+            AddStep("add single ruleset beatmapset", () => carousel.UpdateBeatmapSet(testSingle));
+            AddStep("select filtered map skipping filtered", () => carousel.SelectBeatmap(testSingle.Beatmaps[0], false));
+            checkNoSelection();
+            AddStep("remove single ruleset set", () => carousel.RemoveBeatmapSet(testSingle));
+        }
+
         private BeatmapSetInfo createTestBeatmapSet(int id)
         {
             return new BeatmapSetInfo
diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs
index 287584bf2f..c2bb155753 100644
--- a/osu.Game/Screens/Select/BeatmapCarousel.cs
+++ b/osu.Game/Screens/Select/BeatmapCarousel.cs
@@ -172,20 +172,20 @@ namespace osu.Game.Screens.Select
         /// <summary>
         /// Selects a given beatmap on the carousel.
         ///
-        /// If skipFiltered is true, we will try to select another unfiltered beatmap in the same set. If the
+        /// If bypassFilters is false, we will try to select another unfiltered beatmap in the same set. If the
         /// entire set is filtered, no selection is made.
         /// </summary>
         /// <param name="beatmap">The beatmap to select.</param>
-        /// <param name="skipFiltered">Whether to skip selecting filtered beatmaps.</param>
+        /// <param name="bypassFilters">Whether to select the beatmap even if it is filtered (i.e., not visible on carousel).</param>
         /// <returns>True if a selection was made, False if it wasn't.</returns>
-        public bool SelectBeatmap(BeatmapInfo beatmap, bool skipFiltered = false)
+        public bool SelectBeatmap(BeatmapInfo beatmap, bool bypassFilters = true)
         {
             if (beatmap?.Hidden != false)
                 return false;
 
             foreach (CarouselBeatmapSet set in beatmapSets)
             {
-                if (skipFiltered && set.Filtered)
+                if (!bypassFilters && set.Filtered)
                     continue;
 
                 var item = set.Beatmaps.FirstOrDefault(p => p.Beatmap.Equals(beatmap));
@@ -194,7 +194,7 @@ namespace osu.Game.Screens.Select
                     // The beatmap that needs to be selected doesn't exist in this set
                     continue;
 
-                if (skipFiltered && item.Filtered)
+                if (!bypassFilters && item.Filtered)
                     // The beatmap exists in this set but is filtered, so look for the first unfiltered map in the set
                     item = set.Beatmaps.FirstOrDefault(b => !b.Filtered);
 
diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs
index 461b17338d..b12ab69edd 100644
--- a/osu.Game/Screens/Select/SongSelect.cs
+++ b/osu.Game/Screens/Select/SongSelect.cs
@@ -259,8 +259,13 @@ namespace osu.Game.Screens.Select
 
         protected void WorkingBeatmapChanged(WorkingBeatmap beatmap)
         {
-            if (IsCurrentScreen)
-                Carousel.SelectBeatmap(beatmap?.BeatmapInfo);
+            if (IsCurrentScreen && !Carousel.SelectBeatmap(beatmap?.BeatmapInfo, false))
+                // If selecting new beatmap without bypassing filters failed, there's possibly a ruleset mismatch
+                if (beatmap?.BeatmapInfo?.Ruleset != null && beatmap.BeatmapInfo.Ruleset != Ruleset.Value)
+                {
+                    Ruleset.Value = beatmap.BeatmapInfo.Ruleset;
+                    Carousel.SelectBeatmap(beatmap.BeatmapInfo);
+                }
         }
 
         /// <summary>
@@ -452,7 +457,7 @@ namespace osu.Game.Screens.Select
 
         private void carouselBeatmapsLoaded()
         {
-            if (!Beatmap.IsDefault && Beatmap.Value.BeatmapSetInfo?.DeletePending == false && Beatmap.Value.BeatmapSetInfo?.Protected == false && Carousel.SelectBeatmap(Beatmap.Value.BeatmapInfo, true))
+            if (!Beatmap.IsDefault && Beatmap.Value.BeatmapSetInfo?.DeletePending == false && Beatmap.Value.BeatmapSetInfo?.Protected == false && Carousel.SelectBeatmap(Beatmap.Value.BeatmapInfo, false))
                 return;
 
             if (Carousel.SelectedBeatmapSet == null && !Carousel.SelectNextRandom())

From db2a663234aea7180f577a807fb879c00f4d4908 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Sun, 11 Mar 2018 01:26:03 +0900
Subject: [PATCH 11/16] Use private instead of protected

---
 osu.Game/Screens/Select/SongSelect.cs | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs
index b12ab69edd..ca8a1cae41 100644
--- a/osu.Game/Screens/Select/SongSelect.cs
+++ b/osu.Game/Screens/Select/SongSelect.cs
@@ -214,7 +214,7 @@ namespace osu.Game.Screens.Select
             Beatmap.DisabledChanged += disabled => Carousel.AllowSelection = !disabled;
             Beatmap.TriggerChange();
 
-            Beatmap.ValueChanged += WorkingBeatmapChanged;
+            Beatmap.ValueChanged += workingBeatmapChanged;
         }
 
         public void Edit(BeatmapInfo beatmap)
@@ -257,7 +257,7 @@ namespace osu.Game.Screens.Select
         // We need to keep track of the last selected beatmap ignoring debounce to play the correct selection sounds.
         private BeatmapInfo beatmapNoDebounce;
 
-        protected void WorkingBeatmapChanged(WorkingBeatmap beatmap)
+        private void workingBeatmapChanged(WorkingBeatmap beatmap)
         {
             if (IsCurrentScreen && !Carousel.SelectBeatmap(beatmap?.BeatmapInfo, false))
                 // If selecting new beatmap without bypassing filters failed, there's possibly a ruleset mismatch

From a321bcf3af990a07a97369f7ee1158f06c329a8f Mon Sep 17 00:00:00 2001
From: HoLLy <dario.db@hotmail.com>
Date: Sun, 11 Mar 2018 20:19:03 +0100
Subject: [PATCH 12/16] Fix check against LegacyID for non-default rulesets

---
 osu.Game/Rulesets/RulesetStore.cs                  | 2 +-
 osu.Game/Tests/Visual/TestCasePerformancePoints.cs | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/osu.Game/Rulesets/RulesetStore.cs b/osu.Game/Rulesets/RulesetStore.cs
index 92fbf25f04..4891b46c9d 100644
--- a/osu.Game/Rulesets/RulesetStore.cs
+++ b/osu.Game/Rulesets/RulesetStore.cs
@@ -72,7 +72,7 @@ namespace osu.Game.Rulesets
                 context.SaveChanges();
 
                 //add any other modes
-                foreach (var r in instances.Where(r => r.LegacyID < 0))
+                foreach (var r in instances.Where(r => r.LegacyID == null || r.LegacyID < 0))
                     if (context.RulesetInfo.FirstOrDefault(ri => ri.InstantiationInfo == r.RulesetInfo.InstantiationInfo) == null)
                         context.RulesetInfo.Add(r.RulesetInfo);
 
diff --git a/osu.Game/Tests/Visual/TestCasePerformancePoints.cs b/osu.Game/Tests/Visual/TestCasePerformancePoints.cs
index 5b32433467..c35c0308f9 100644
--- a/osu.Game/Tests/Visual/TestCasePerformancePoints.cs
+++ b/osu.Game/Tests/Visual/TestCasePerformancePoints.cs
@@ -128,7 +128,7 @@ namespace osu.Game.Tests.Visual
             private void load(BeatmapManager beatmaps)
             {
                 var sets = beatmaps.GetAllUsableBeatmapSets();
-                var allBeatmaps = sets.SelectMany(s => s.Beatmaps).Where(b => ruleset.LegacyID < 0 || b.RulesetID == ruleset.LegacyID);
+                var allBeatmaps = sets.SelectMany(s => s.Beatmaps).Where(b => ruleset.LegacyID == null || ruleset.LegacyID < 0 || b.RulesetID == ruleset.LegacyID);
 
                 allBeatmaps.ForEach(b => beatmapDisplays.Add(new BeatmapDisplay(b)));
             }

From 9c75c392f2edb4452f48a2e7ab9bb4e2b40de47e Mon Sep 17 00:00:00 2001
From: HoLLy <dario.db@hotmail.com>
Date: Sun, 11 Mar 2018 21:27:49 +0100
Subject: [PATCH 13/16] Only check by null instead of sign

---
 osu.Game/Rulesets/RulesetStore.cs                  | 4 ++--
 osu.Game/Tests/Visual/TestCasePerformancePoints.cs | 2 +-
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/osu.Game/Rulesets/RulesetStore.cs b/osu.Game/Rulesets/RulesetStore.cs
index 4891b46c9d..e621c3cf2b 100644
--- a/osu.Game/Rulesets/RulesetStore.cs
+++ b/osu.Game/Rulesets/RulesetStore.cs
@@ -63,7 +63,7 @@ namespace osu.Game.Rulesets
                 var instances = loaded_assemblies.Values.Select(r => (Ruleset)Activator.CreateInstance(r, (RulesetInfo)null)).ToList();
 
                 //add all legacy modes in correct order
-                foreach (var r in instances.Where(r => r.LegacyID >= 0).OrderBy(r => r.LegacyID))
+                foreach (var r in instances.Where(r => r.LegacyID != null).OrderBy(r => r.LegacyID))
                 {
                     if (context.RulesetInfo.SingleOrDefault(rsi => rsi.ID == r.RulesetInfo.ID) == null)
                         context.RulesetInfo.Add(r.RulesetInfo);
@@ -72,7 +72,7 @@ namespace osu.Game.Rulesets
                 context.SaveChanges();
 
                 //add any other modes
-                foreach (var r in instances.Where(r => r.LegacyID == null || r.LegacyID < 0))
+                foreach (var r in instances.Where(r => r.LegacyID == null))
                     if (context.RulesetInfo.FirstOrDefault(ri => ri.InstantiationInfo == r.RulesetInfo.InstantiationInfo) == null)
                         context.RulesetInfo.Add(r.RulesetInfo);
 
diff --git a/osu.Game/Tests/Visual/TestCasePerformancePoints.cs b/osu.Game/Tests/Visual/TestCasePerformancePoints.cs
index c35c0308f9..7ca69c14b8 100644
--- a/osu.Game/Tests/Visual/TestCasePerformancePoints.cs
+++ b/osu.Game/Tests/Visual/TestCasePerformancePoints.cs
@@ -128,7 +128,7 @@ namespace osu.Game.Tests.Visual
             private void load(BeatmapManager beatmaps)
             {
                 var sets = beatmaps.GetAllUsableBeatmapSets();
-                var allBeatmaps = sets.SelectMany(s => s.Beatmaps).Where(b => ruleset.LegacyID == null || ruleset.LegacyID < 0 || b.RulesetID == ruleset.LegacyID);
+                var allBeatmaps = sets.SelectMany(s => s.Beatmaps).Where(b => ruleset.LegacyID == null || b.RulesetID == ruleset.LegacyID);
 
                 allBeatmaps.ForEach(b => beatmapDisplays.Add(new BeatmapDisplay(b)));
             }

From 33c721bcbb51d50f9d60d656208eb31538f7b6b2 Mon Sep 17 00:00:00 2001
From: smoogipoo <smoogipoo@smgi.me>
Date: Mon, 12 Mar 2018 09:51:32 +0900
Subject: [PATCH 14/16] Fix post-merge errors

---
 osu-framework                   | 2 +-
 osu.Game/Screens/Play/Player.cs | 1 -
 2 files changed, 1 insertion(+), 2 deletions(-)

diff --git a/osu-framework b/osu-framework
index 865b0df18b..59004b46f2 160000
--- a/osu-framework
+++ b/osu-framework
@@ -1 +1 @@
-Subproject commit 865b0df18bb240190cdf7a7f60d44c0b28c84c5f
+Subproject commit 59004b46f2c96ac02fec712e66f9f96fe252f2fa
diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs
index 6453a72fc3..c8ff261a93 100644
--- a/osu.Game/Screens/Play/Player.cs
+++ b/osu.Game/Screens/Play/Player.cs
@@ -25,7 +25,6 @@ using osu.Game.Rulesets;
 using osu.Game.Rulesets.Mods;
 using osu.Game.Rulesets.Scoring;
 using osu.Game.Rulesets.UI;
-using osu.Game.Screens.Backgrounds;
 using osu.Game.Screens.Ranking;
 using osu.Game.Storyboards.Drawables;
 

From 209d91fe2131a8a9c8fd62f5be117bd6b2a69331 Mon Sep 17 00:00:00 2001
From: smoogipoo <smoogipoo@smgi.me>
Date: Mon, 12 Mar 2018 10:35:37 +0900
Subject: [PATCH 15/16] Fix duplicate item in csproj

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

diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj
index 562f0ff788..4943db6852 100644
--- a/osu.Game/osu.Game.csproj
+++ b/osu.Game/osu.Game.csproj
@@ -379,7 +379,6 @@
     <Compile Include="Rulesets\Replays\Types\IConvertibleReplayFrame.cs" />
     <Compile Include="Rulesets\Scoring\Legacy\LegacyScoreParser.cs" />
     <Compile Include="Rulesets\UI\JudgementContainer.cs" />
-    <Compile Include="Rulesets\UI\ScalableContainer.cs" />
     <Compile Include="Screens\Play\ScreenWithBeatmapBackground.cs" />
     <Compile Include="Screens\Play\PlayerSettings\VisualSettings.cs" />
     <Compile Include="Rulesets\Objects\CatmullApproximator.cs" />

From 86d93ffe3c59a3b1fa57d4a8855298ac3ecc39b9 Mon Sep 17 00:00:00 2001
From: smoogipoo <smoogipoo@smgi.me>
Date: Mon, 12 Mar 2018 10:49:50 +0900
Subject: [PATCH 16/16] Fix tooltip not working due to not handling input

---
 osu.Game/Graphics/DrawableDate.cs | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/osu.Game/Graphics/DrawableDate.cs b/osu.Game/Graphics/DrawableDate.cs
index 452443f9d0..a912f989e0 100644
--- a/osu.Game/Graphics/DrawableDate.cs
+++ b/osu.Game/Graphics/DrawableDate.cs
@@ -58,6 +58,8 @@ namespace osu.Game.Graphics
             Scheduler.AddDelayed(updateTimeWithReschedule, timeUntilNextUpdate);
         }
 
+        public override bool HandleMouseInput => true;
+
         private void updateTime() => Text = date.Humanize();
         public string TooltipText => date.ToString();
     }