diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneNotes.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneNotes.cs
index 0d143198dd..b2613a59d5 100644
--- a/osu.Game.Rulesets.Mania.Tests/TestSceneNotes.cs
+++ b/osu.Game.Rulesets.Mania.Tests/TestSceneNotes.cs
@@ -11,10 +11,10 @@ using osu.Framework.Extensions.Color4Extensions;
 using osu.Framework.Graphics;
 using osu.Framework.Graphics.Containers;
 using osu.Framework.Graphics.Shapes;
-using osu.Framework.Graphics.Sprites;
 using osu.Game.Beatmaps;
 using osu.Game.Beatmaps.ControlPoints;
 using osu.Game.Graphics;
+using osu.Game.Graphics.Sprites;
 using osu.Game.Rulesets.Mania.Objects;
 using osu.Game.Rulesets.Mania.Objects.Drawables;
 using osu.Game.Rulesets.Objects.Drawables;
@@ -138,7 +138,7 @@ namespace osu.Game.Rulesets.Mania.Tests
                                 content = new Container { RelativeSizeAxes = Axes.Both }
                             }
                         },
-                        new SpriteText
+                        new OsuSpriteText
                         {
                             Anchor = Anchor.TopCentre,
                             Origin = Anchor.TopCentre,
diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs
index 3c1f48b7a4..5fd5fe342d 100644
--- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs
+++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs
@@ -170,27 +170,98 @@ namespace osu.Game.Tests.Beatmaps.Formats
                 var controlPoints = beatmap.ControlPointInfo;
 
                 Assert.AreEqual(4, controlPoints.TimingPoints.Count);
-                var timingPoint = controlPoints.TimingPoints[0];
+                Assert.AreEqual(42, controlPoints.DifficultyPoints.Count);
+                Assert.AreEqual(42, controlPoints.SamplePoints.Count);
+                Assert.AreEqual(42, controlPoints.EffectPoints.Count);
+
+                var timingPoint = controlPoints.TimingPointAt(0);
+                Assert.AreEqual(956, timingPoint.Time);
+                Assert.AreEqual(329.67032967033, timingPoint.BeatLength);
+                Assert.AreEqual(TimeSignatures.SimpleQuadruple, timingPoint.TimeSignature);
+
+                timingPoint = controlPoints.TimingPointAt(48428);
                 Assert.AreEqual(956, timingPoint.Time);
                 Assert.AreEqual(329.67032967033d, timingPoint.BeatLength);
                 Assert.AreEqual(TimeSignatures.SimpleQuadruple, timingPoint.TimeSignature);
 
-                Assert.AreEqual(5, controlPoints.DifficultyPoints.Count);
-                var difficultyPoint = controlPoints.DifficultyPoints[0];
-                Assert.AreEqual(116999, difficultyPoint.Time);
-                Assert.AreEqual(0.75000000000000189d, difficultyPoint.SpeedMultiplier);
+                timingPoint = controlPoints.TimingPointAt(119637);
+                Assert.AreEqual(119637, timingPoint.Time);
+                Assert.AreEqual(659.340659340659, timingPoint.BeatLength);
+                Assert.AreEqual(TimeSignatures.SimpleQuadruple, timingPoint.TimeSignature);
 
-                Assert.AreEqual(34, controlPoints.SamplePoints.Count);
-                var soundPoint = controlPoints.SamplePoints[0];
+                var difficultyPoint = controlPoints.DifficultyPointAt(0);
+                Assert.AreEqual(0, difficultyPoint.Time);
+                Assert.AreEqual(1.0, difficultyPoint.SpeedMultiplier);
+
+                difficultyPoint = controlPoints.DifficultyPointAt(48428);
+                Assert.AreEqual(48428, difficultyPoint.Time);
+                Assert.AreEqual(1.0, difficultyPoint.SpeedMultiplier);
+
+                difficultyPoint = controlPoints.DifficultyPointAt(116999);
+                Assert.AreEqual(116999, difficultyPoint.Time);
+                Assert.AreEqual(0.75, difficultyPoint.SpeedMultiplier, 0.1);
+
+                var soundPoint = controlPoints.SamplePointAt(0);
                 Assert.AreEqual(956, soundPoint.Time);
                 Assert.AreEqual("soft", soundPoint.SampleBank);
                 Assert.AreEqual(60, soundPoint.SampleVolume);
 
-                Assert.AreEqual(8, controlPoints.EffectPoints.Count);
-                var effectPoint = controlPoints.EffectPoints[0];
+                soundPoint = controlPoints.SamplePointAt(53373);
+                Assert.AreEqual(53373, soundPoint.Time);
+                Assert.AreEqual("soft", soundPoint.SampleBank);
+                Assert.AreEqual(60, soundPoint.SampleVolume);
+
+                soundPoint = controlPoints.SamplePointAt(119637);
+                Assert.AreEqual(119637, soundPoint.Time);
+                Assert.AreEqual("soft", soundPoint.SampleBank);
+                Assert.AreEqual(80, soundPoint.SampleVolume);
+
+                var effectPoint = controlPoints.EffectPointAt(0);
+                Assert.AreEqual(0, effectPoint.Time);
+                Assert.IsFalse(effectPoint.KiaiMode);
+                Assert.IsFalse(effectPoint.OmitFirstBarLine);
+
+                effectPoint = controlPoints.EffectPointAt(53703);
                 Assert.AreEqual(53703, effectPoint.Time);
                 Assert.IsTrue(effectPoint.KiaiMode);
                 Assert.IsFalse(effectPoint.OmitFirstBarLine);
+
+                effectPoint = controlPoints.EffectPointAt(119637);
+                Assert.AreEqual(119637, effectPoint.Time);
+                Assert.IsFalse(effectPoint.KiaiMode);
+                Assert.IsFalse(effectPoint.OmitFirstBarLine);
+            }
+        }
+
+        [Test]
+        public void TestDecodeOverlappingTimingPoints()
+        {
+            var decoder = new LegacyBeatmapDecoder { ApplyOffsets = false };
+
+            using (var resStream = TestResources.OpenResource("overlapping-control-points.osu"))
+            using (var stream = new StreamReader(resStream))
+            {
+                var controlPoints = decoder.Decode(stream).ControlPointInfo;
+
+                Assert.That(controlPoints.DifficultyPointAt(500).SpeedMultiplier, Is.EqualTo(1.5).Within(0.1));
+                Assert.That(controlPoints.DifficultyPointAt(1500).SpeedMultiplier, Is.EqualTo(1.5).Within(0.1));
+                Assert.That(controlPoints.DifficultyPointAt(2500).SpeedMultiplier, Is.EqualTo(0.75).Within(0.1));
+                Assert.That(controlPoints.DifficultyPointAt(3500).SpeedMultiplier, Is.EqualTo(1.5).Within(0.1));
+
+                Assert.That(controlPoints.EffectPointAt(500).KiaiMode, Is.True);
+                Assert.That(controlPoints.EffectPointAt(1500).KiaiMode, Is.True);
+                Assert.That(controlPoints.EffectPointAt(2500).KiaiMode, Is.False);
+                Assert.That(controlPoints.EffectPointAt(3500).KiaiMode, Is.True);
+
+                Assert.That(controlPoints.SamplePointAt(500).SampleBank, Is.EqualTo("drum"));
+                Assert.That(controlPoints.SamplePointAt(1500).SampleBank, Is.EqualTo("drum"));
+                Assert.That(controlPoints.SamplePointAt(2500).SampleBank, Is.EqualTo("normal"));
+                Assert.That(controlPoints.SamplePointAt(3500).SampleBank, Is.EqualTo("drum"));
+
+                Assert.That(controlPoints.TimingPointAt(500).BeatLength, Is.EqualTo(500).Within(0.1));
+                Assert.That(controlPoints.TimingPointAt(1500).BeatLength, Is.EqualTo(500).Within(0.1));
+                Assert.That(controlPoints.TimingPointAt(2500).BeatLength, Is.EqualTo(250).Within(0.1));
+                Assert.That(controlPoints.TimingPointAt(3500).BeatLength, Is.EqualTo(500).Within(0.1));
             }
         }
 
diff --git a/osu.Game.Tests/Resources/overlapping-control-points.osu b/osu.Game.Tests/Resources/overlapping-control-points.osu
new file mode 100644
index 0000000000..31d38a3d01
--- /dev/null
+++ b/osu.Game.Tests/Resources/overlapping-control-points.osu
@@ -0,0 +1,19 @@
+osu file format v14
+
+[TimingPoints]
+
+// Timing then inherited
+0,500,4,2,0,100,1,0
+0,-66.6666666666667,4,3,0,100,0,1
+
+// Inherited then timing (equivalent to previous)
+1000,-66.6666666666667,4,3,0,100,0,1
+1000,500,4,2,0,100,1,0
+
+// Inherited then timing (different to previous)
+2000,-133.333333333333,4,1,0,100,0,0
+2000,250,4,2,0,100,1,0
+
+// Timing then inherited (different to previous)
+3000,500,4,2,0,100,1,0
+3000,-66.6666666666667,4,3,0,100,0,1
diff --git a/osu.Game.Tests/Visual/Background/TestSceneBackgroundScreenBeatmap.cs b/osu.Game.Tests/Visual/Background/TestSceneBackgroundScreenBeatmap.cs
index 10e3dc10c8..c9bdcf928f 100644
--- a/osu.Game.Tests/Visual/Background/TestSceneBackgroundScreenBeatmap.cs
+++ b/osu.Game.Tests/Visual/Background/TestSceneBackgroundScreenBeatmap.cs
@@ -9,7 +9,6 @@ using NUnit.Framework;
 using osu.Framework.Allocation;
 using osu.Framework.Bindables;
 using osu.Framework.Graphics;
-using osu.Framework.Graphics.Sprites;
 using osu.Framework.Input.Events;
 using osu.Framework.Input.States;
 using osu.Framework.Platform;
@@ -19,6 +18,7 @@ using osu.Game.Configuration;
 using osu.Game.Database;
 using osu.Game.Graphics;
 using osu.Game.Graphics.Containers;
+using osu.Game.Graphics.Sprites;
 using osu.Game.Rulesets;
 using osu.Game.Rulesets.Osu.Mods;
 using osu.Game.Scoring;
@@ -240,7 +240,7 @@ namespace osu.Game.Tests.Visual.Background
         {
             player.StoryboardEnabled.Value = false;
             player.ReplacesBackground.Value = false;
-            player.CurrentStoryboardContainer.Add(new SpriteText
+            player.CurrentStoryboardContainer.Add(new OsuSpriteText
             {
                 Size = new Vector2(250, 50),
                 Alpha = 1,
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneScoreCounter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneScoreCounter.cs
index 0c9e3fcd73..080a287b48 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneScoreCounter.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneScoreCounter.cs
@@ -5,6 +5,7 @@ using NUnit.Framework;
 using osu.Framework.Graphics;
 using osu.Framework.Graphics.Sprites;
 using osu.Framework.MathUtils;
+using osu.Game.Graphics.Sprites;
 using osu.Game.Graphics.UserInterface;
 using osu.Game.Screens.Play.HUD;
 using osuTK;
@@ -53,7 +54,7 @@ namespace osu.Game.Tests.Visual.Gameplay
             };
             Add(stars);
 
-            SpriteText starsLabel = new SpriteText
+            SpriteText starsLabel = new OsuSpriteText
             {
                 Origin = Anchor.BottomLeft,
                 Anchor = Anchor.BottomLeft,
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinReloadable.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinReloadable.cs
index 7d6edd0d12..c7a0df6e9f 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinReloadable.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinReloadable.cs
@@ -7,9 +7,9 @@ using osu.Framework.Audio.Sample;
 using osu.Framework.Graphics;
 using osu.Framework.Graphics.Containers;
 using osu.Framework.Graphics.Shapes;
-using osu.Framework.Graphics.Sprites;
 using osu.Framework.Graphics.Textures;
 using osu.Game.Graphics;
+using osu.Game.Graphics.Sprites;
 using osu.Game.Skinning;
 using osuTK.Graphics;
 
@@ -76,7 +76,7 @@ namespace osu.Game.Tests.Visual.Gameplay
                         Colour = Color4.Black,
                         RelativeSizeAxes = Axes.Both,
                     },
-                    new SpriteText
+                    new OsuSpriteText
                     {
                         Font = OsuFont.Default.With(size: 40),
                         Anchor = Anchor.Centre,
diff --git a/osu.Game.Tests/Visual/Menus/TestSceneLoaderAnimation.cs b/osu.Game.Tests/Visual/Menus/TestSceneLoaderAnimation.cs
index 813d4df708..000832b784 100644
--- a/osu.Game.Tests/Visual/Menus/TestSceneLoaderAnimation.cs
+++ b/osu.Game.Tests/Visual/Menus/TestSceneLoaderAnimation.cs
@@ -53,38 +53,12 @@ namespace osu.Game.Tests.Visual.Menus
         }
 
         [Test]
-        public void TestShortLoad()
+        public void TestDelayedLoad()
         {
-            bool logoVisible = false;
-
             AddStep("begin loading", () => LoadScreen(loader = new TestLoader()));
-            AddWaitStep("wait", 3);
-            AddStep("finish loading", () =>
-            {
-                logoVisible = loader.Logo?.Alpha > 0;
-                loader.AllowLoad.Set();
-            });
-
+            AddUntilStep("wait for logo visible", () => loader.Logo?.Alpha > 0);
+            AddStep("finish loading", () => loader.AllowLoad.Set());
             AddAssert("loaded", () => loader.Logo != null && loader.ScreenLoaded);
-            AddAssert("logo was visible", () => logoVisible);
-            AddUntilStep("logo gone", () => loader.Logo?.Alpha == 0);
-        }
-
-        [Test]
-        public void TestLongLoad()
-        {
-            bool logoVisible = false;
-
-            AddStep("begin loading", () => LoadScreen(loader = new TestLoader()));
-            AddWaitStep("wait", 10);
-            AddStep("finish loading", () =>
-            {
-                logoVisible = loader.Logo?.Alpha > 0;
-                loader.AllowLoad.Set();
-            });
-
-            AddAssert("loaded", () => loader.Logo != null && loader.ScreenLoaded);
-            AddAssert("logo was visible", () => logoVisible);
             AddUntilStep("logo gone", () => loader.Logo?.Alpha == 0);
         }
 
diff --git a/osu.Game.Tests/Visual/Online/TestSceneChannelTabControl.cs b/osu.Game.Tests/Visual/Online/TestSceneChannelTabControl.cs
index d93daba4d4..364c986723 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneChannelTabControl.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneChannelTabControl.cs
@@ -10,6 +10,7 @@ using osu.Framework.Graphics.Containers;
 using osu.Framework.Graphics.Shapes;
 using osu.Framework.Graphics.Sprites;
 using osu.Framework.MathUtils;
+using osu.Game.Graphics.Sprites;
 using osu.Game.Online.Chat;
 using osu.Game.Overlays.Chat.Tabs;
 using osu.Game.Users;
@@ -61,7 +62,7 @@ namespace osu.Game.Tests.Visual.Online
                 Anchor = Anchor.TopLeft,
                 Children = new Drawable[]
                 {
-                    currentText = new SpriteText
+                    currentText = new OsuSpriteText
                     {
                         Text = "Currently selected channel:"
                     }
diff --git a/osu.Game.Tests/Visual/TestSceneOsuScreenStack.cs b/osu.Game.Tests/Visual/TestSceneOsuScreenStack.cs
index 53ce25ebb3..a68fd0ef40 100644
--- a/osu.Game.Tests/Visual/TestSceneOsuScreenStack.cs
+++ b/osu.Game.Tests/Visual/TestSceneOsuScreenStack.cs
@@ -4,9 +4,9 @@
 using NUnit.Framework;
 using osu.Framework.Allocation;
 using osu.Framework.Graphics;
-using osu.Framework.Graphics.Sprites;
 using osu.Framework.Screens;
 using osu.Framework.Testing;
+using osu.Game.Graphics.Sprites;
 using osu.Game.Screens;
 using osu.Game.Screens.Play;
 using osuTK.Graphics;
@@ -54,7 +54,7 @@ namespace osu.Game.Tests.Visual
             [BackgroundDependencyLoader]
             private void load()
             {
-                AddInternal(new SpriteText
+                AddInternal(new OsuSpriteText
                 {
                     Text = screenText,
                     Colour = Color4.White,
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs
index 0cb7c2484d..71033fcd2f 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs
@@ -9,6 +9,7 @@ using osu.Framework.Graphics;
 using osu.Framework.Graphics.Containers;
 using osu.Framework.Graphics.Sprites;
 using osu.Framework.MathUtils;
+using osu.Game.Graphics.Sprites;
 using osu.Game.Overlays;
 using osu.Game.Overlays.Notifications;
 
@@ -40,7 +41,7 @@ namespace osu.Game.Tests.Visual.UserInterface
                 Origin = Anchor.TopRight
             });
 
-            SpriteText displayedCount = new SpriteText();
+            SpriteText displayedCount = new OsuSpriteText();
 
             Content.Add(displayedCount);
 
diff --git a/osu.Game/Beatmaps/ControlPoints/ControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/ControlPoint.cs
index 825b60ae5f..abe7e5e803 100644
--- a/osu.Game/Beatmaps/ControlPoints/ControlPoint.cs
+++ b/osu.Game/Beatmaps/ControlPoints/ControlPoint.cs
@@ -12,17 +12,14 @@ namespace osu.Game.Beatmaps.ControlPoints
         /// </summary>
         public double Time;
 
+        /// <summary>
+        /// Whether this timing point was generated internally, as opposed to parsed from the underlying beatmap.
+        /// </summary>
+        internal bool AutoGenerated;
+
         public int CompareTo(ControlPoint other) => Time.CompareTo(other.Time);
 
-        /// <summary>
-        /// Whether this <see cref="ControlPoint"/> provides the same parametric changes as another <see cref="ControlPoint"/>.
-        /// Basically an equality check without considering the <see cref="Time"/>.
-        /// </summary>
-        /// <param name="other">The <see cref="ControlPoint"/> to compare to.</param>
-        /// <returns>Whether this <see cref="ControlPoint"/> is equivalent to <paramref name="other"/>.</returns>
-        public virtual bool EquivalentTo(ControlPoint other) => true;
-
         public bool Equals(ControlPoint other)
-            => EquivalentTo(other) && Time.Equals(other?.Time);
+            => Time.Equals(other?.Time);
     }
 }
diff --git a/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs
index 013271d597..a3e3121575 100644
--- a/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs
+++ b/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs
@@ -1,11 +1,12 @@
 // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
 // See the LICENCE file in the repository root for full licence text.
 
+using System;
 using osuTK;
 
 namespace osu.Game.Beatmaps.ControlPoints
 {
-    public class DifficultyControlPoint : ControlPoint
+    public class DifficultyControlPoint : ControlPoint, IEquatable<DifficultyControlPoint>
     {
         /// <summary>
         /// The speed multiplier at this control point.
@@ -18,9 +19,8 @@ namespace osu.Game.Beatmaps.ControlPoints
 
         private double speedMultiplier = 1;
 
-        public override bool EquivalentTo(ControlPoint other)
-            => base.EquivalentTo(other)
-               && other is DifficultyControlPoint difficulty
-               && SpeedMultiplier.Equals(difficulty.SpeedMultiplier);
+        public bool Equals(DifficultyControlPoint other)
+            => base.Equals(other)
+               && SpeedMultiplier.Equals(other?.SpeedMultiplier);
     }
 }
diff --git a/osu.Game/Beatmaps/ControlPoints/EffectControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/EffectControlPoint.cs
index 3978b7b4b0..354d86dc13 100644
--- a/osu.Game/Beatmaps/ControlPoints/EffectControlPoint.cs
+++ b/osu.Game/Beatmaps/ControlPoints/EffectControlPoint.cs
@@ -1,9 +1,11 @@
 // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
 // See the LICENCE file in the repository root for full licence text.
 
+using System;
+
 namespace osu.Game.Beatmaps.ControlPoints
 {
-    public class EffectControlPoint : ControlPoint
+    public class EffectControlPoint : ControlPoint, IEquatable<EffectControlPoint>
     {
         /// <summary>
         /// Whether this control point enables Kiai mode.
@@ -15,10 +17,8 @@ namespace osu.Game.Beatmaps.ControlPoints
         /// </summary>
         public bool OmitFirstBarLine;
 
-        public override bool EquivalentTo(ControlPoint other)
-            => base.EquivalentTo(other)
-               && other is EffectControlPoint effect
-               && KiaiMode.Equals(effect.KiaiMode)
-               && OmitFirstBarLine.Equals(effect.OmitFirstBarLine);
+        public bool Equals(EffectControlPoint other)
+            => base.Equals(other)
+               && KiaiMode == other?.KiaiMode && OmitFirstBarLine == other.OmitFirstBarLine;
     }
 }
diff --git a/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs
index 241ce90740..4c45bef862 100644
--- a/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs
+++ b/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs
@@ -1,11 +1,12 @@
 // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
 // See the LICENCE file in the repository root for full licence text.
 
+using System;
 using osu.Game.Audio;
 
 namespace osu.Game.Beatmaps.ControlPoints
 {
-    public class SampleControlPoint : ControlPoint
+    public class SampleControlPoint : ControlPoint, IEquatable<SampleControlPoint>
     {
         public const string DEFAULT_BANK = "normal";
 
@@ -44,10 +45,8 @@ namespace osu.Game.Beatmaps.ControlPoints
             return newSampleInfo;
         }
 
-        public override bool EquivalentTo(ControlPoint other)
-            => base.EquivalentTo(other)
-               && other is SampleControlPoint sample
-               && SampleBank.Equals(sample.SampleBank)
-               && SampleVolume.Equals(sample.SampleVolume);
+        public bool Equals(SampleControlPoint other)
+            => base.Equals(other)
+               && string.Equals(SampleBank, other?.SampleBank) && SampleVolume == other?.SampleVolume;
     }
 }
diff --git a/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs
index 9ec27bdfdf..e5815a3f3b 100644
--- a/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs
+++ b/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs
@@ -1,12 +1,13 @@
 // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
 // See the LICENCE file in the repository root for full licence text.
 
+using System;
 using osuTK;
 using osu.Game.Beatmaps.Timing;
 
 namespace osu.Game.Beatmaps.ControlPoints
 {
-    public class TimingControlPoint : ControlPoint
+    public class TimingControlPoint : ControlPoint, IEquatable<TimingControlPoint>
     {
         /// <summary>
         /// The time signature at this control point.
@@ -24,10 +25,8 @@ namespace osu.Game.Beatmaps.ControlPoints
 
         private double beatLength = 1000;
 
-        public override bool EquivalentTo(ControlPoint other)
-            => base.EquivalentTo(other)
-               && other is TimingControlPoint timing
-               && TimeSignature.Equals(timing.TimeSignature)
-               && BeatLength.Equals(timing.BeatLength);
+        public bool Equals(TimingControlPoint other)
+            => base.Equals(other)
+               && TimeSignature == other?.TimeSignature && beatLength.Equals(other.beatLength);
     }
 }
diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs
index b489b5e6d9..3cd425ea44 100644
--- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs
+++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs
@@ -374,14 +374,16 @@ namespace osu.Game.Beatmaps.Formats
                 handleDifficultyControlPoint(new DifficultyControlPoint
                 {
                     Time = time,
-                    SpeedMultiplier = speedMultiplier
+                    SpeedMultiplier = speedMultiplier,
+                    AutoGenerated = timingChange
                 });
 
                 handleEffectControlPoint(new EffectControlPoint
                 {
                     Time = time,
                     KiaiMode = kiaiMode,
-                    OmitFirstBarLine = omitFirstBarSignature
+                    OmitFirstBarLine = omitFirstBarSignature,
+                    AutoGenerated = timingChange
                 });
 
                 handleSampleControlPoint(new LegacySampleControlPoint
@@ -389,7 +391,8 @@ namespace osu.Game.Beatmaps.Formats
                     Time = time,
                     SampleBank = stringSampleSet,
                     SampleVolume = sampleVolume,
-                    CustomSampleBank = customSampleBank
+                    CustomSampleBank = customSampleBank,
+                    AutoGenerated = timingChange
                 });
             }
             catch (FormatException)
@@ -407,7 +410,14 @@ namespace osu.Game.Beatmaps.Formats
             var existing = beatmap.ControlPointInfo.TimingPointAt(newPoint.Time);
 
             if (existing.Time == newPoint.Time)
+            {
+                // autogenerated points should not replace non-autogenerated.
+                // this allows for incorrectly ordered timing points to still be correctly handled.
+                if (newPoint.AutoGenerated && !existing.AutoGenerated)
+                    return;
+
                 beatmap.ControlPointInfo.TimingPoints.Remove(existing);
+            }
 
             beatmap.ControlPointInfo.TimingPoints.Add(newPoint);
         }
@@ -416,11 +426,15 @@ namespace osu.Game.Beatmaps.Formats
         {
             var existing = beatmap.ControlPointInfo.DifficultyPointAt(newPoint.Time);
 
-            if (newPoint.EquivalentTo(existing))
-                return;
-
             if (existing.Time == newPoint.Time)
+            {
+                // autogenerated points should not replace non-autogenerated.
+                // this allows for incorrectly ordered timing points to still be correctly handled.
+                if (newPoint.AutoGenerated && !existing.AutoGenerated)
+                    return;
+
                 beatmap.ControlPointInfo.DifficultyPoints.Remove(existing);
+            }
 
             beatmap.ControlPointInfo.DifficultyPoints.Add(newPoint);
         }
@@ -429,11 +443,15 @@ namespace osu.Game.Beatmaps.Formats
         {
             var existing = beatmap.ControlPointInfo.EffectPointAt(newPoint.Time);
 
-            if (newPoint.EquivalentTo(existing))
-                return;
-
             if (existing.Time == newPoint.Time)
+            {
+                // autogenerated points should not replace non-autogenerated.
+                // this allows for incorrectly ordered timing points to still be correctly handled.
+                if (newPoint.AutoGenerated && !existing.AutoGenerated)
+                    return;
+
                 beatmap.ControlPointInfo.EffectPoints.Remove(existing);
+            }
 
             beatmap.ControlPointInfo.EffectPoints.Add(newPoint);
         }
@@ -442,11 +460,15 @@ namespace osu.Game.Beatmaps.Formats
         {
             var existing = beatmap.ControlPointInfo.SamplePointAt(newPoint.Time);
 
-            if (newPoint.EquivalentTo(existing))
-                return;
-
             if (existing.Time == newPoint.Time)
+            {
+                // autogenerated points should not replace non-autogenerated.
+                // this allows for incorrectly ordered timing points to still be correctly handled.
+                if (newPoint.AutoGenerated && !existing.AutoGenerated)
+                    return;
+
                 beatmap.ControlPointInfo.SamplePoints.Remove(existing);
+            }
 
             beatmap.ControlPointInfo.SamplePoints.Add(newPoint);
         }
diff --git a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs
index eb5bcfe824..7b7e0e7101 100644
--- a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs
+++ b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs
@@ -189,7 +189,7 @@ namespace osu.Game.Beatmaps.Formats
             Foreground = 3
         }
 
-        internal class LegacySampleControlPoint : SampleControlPoint
+        internal class LegacySampleControlPoint : SampleControlPoint, IEquatable<LegacySampleControlPoint>
         {
             public int CustomSampleBank;
 
@@ -203,10 +203,9 @@ namespace osu.Game.Beatmaps.Formats
                 return baseInfo;
             }
 
-            public override bool EquivalentTo(ControlPoint other)
-                => base.EquivalentTo(other)
-                   && other is LegacySampleControlPoint legacy
-                   && CustomSampleBank == legacy.CustomSampleBank;
+            public bool Equals(LegacySampleControlPoint other)
+                => base.Equals(other)
+                   && CustomSampleBank == other?.CustomSampleBank;
         }
     }
 }
diff --git a/osu.Game/Graphics/Containers/OsuClickableContainer.cs b/osu.Game/Graphics/Containers/OsuClickableContainer.cs
index e4d30cebb7..6dbe340efb 100644
--- a/osu.Game/Graphics/Containers/OsuClickableContainer.cs
+++ b/osu.Game/Graphics/Containers/OsuClickableContainer.cs
@@ -4,11 +4,12 @@
 using osu.Framework.Allocation;
 using osu.Framework.Graphics;
 using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Cursor;
 using osu.Game.Graphics.UserInterface;
 
 namespace osu.Game.Graphics.Containers
 {
-    public class OsuClickableContainer : ClickableContainer
+    public class OsuClickableContainer : ClickableContainer, IHasTooltip
     {
         private readonly HoverSampleSet sampleSet;
 
@@ -23,6 +24,8 @@ namespace osu.Game.Graphics.Containers
             this.sampleSet = sampleSet;
         }
 
+        public virtual string TooltipText { get; set; }
+
         [BackgroundDependencyLoader]
         private void load()
         {
diff --git a/osu.Game/Graphics/OsuColour.cs b/osu.Game/Graphics/OsuColour.cs
index a73a8bcbc1..53693a1e38 100644
--- a/osu.Game/Graphics/OsuColour.cs
+++ b/osu.Game/Graphics/OsuColour.cs
@@ -136,15 +136,5 @@ namespace osu.Game.Graphics
         public readonly Color4 ChatBlue = FromHex(@"17292e");
 
         public readonly Color4 ContextMenuGray = FromHex(@"223034");
-
-        public readonly Color4 CommunityUserGreenLight = FromHex(@"deff87");
-        public readonly Color4 CommunityUserGreen = FromHex(@"05ffa2");
-        public readonly Color4 CommunityUserGreenDark = FromHex(@"a6cc00");
-        public readonly Color4 CommunityUserGrayGreenLighter = FromHex(@"9ebab1");
-        public readonly Color4 CommunityUserGrayGreenLight = FromHex(@"77998e");
-        public readonly Color4 CommunityUserGrayGreen = FromHex(@"4e7466");
-        public readonly Color4 CommunityUserGrayGreenDark = FromHex(@"33413c");
-        public readonly Color4 CommunityUserGrayGreenDarker = FromHex(@"2c3532");
-        public readonly Color4 CommunityUserGrayGreenDarkest = FromHex(@"1e2422");
     }
 }
diff --git a/osu.Game/Online/Chat/DrawableLinkCompiler.cs b/osu.Game/Online/Chat/DrawableLinkCompiler.cs
index d34ec8091c..d27a3fbffe 100644
--- a/osu.Game/Online/Chat/DrawableLinkCompiler.cs
+++ b/osu.Game/Online/Chat/DrawableLinkCompiler.cs
@@ -1,7 +1,6 @@
 // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
 // See the LICENCE file in the repository root for full licence text.
 
-using osu.Framework.Graphics.Cursor;
 using System.Collections.Generic;
 using System.Linq;
 using osu.Framework.Allocation;
@@ -16,7 +15,7 @@ namespace osu.Game.Online.Chat
     /// <summary>
     /// An invisible drawable that brings multiple <see cref="Drawable"/> pieces together to form a consumable clickable link.
     /// </summary>
-    public class DrawableLinkCompiler : OsuHoverContainer, IHasTooltip
+    public class DrawableLinkCompiler : OsuHoverContainer
     {
         /// <summary>
         /// Each word part of a chat link (split for word-wrap support).
@@ -40,8 +39,6 @@ namespace osu.Game.Online.Chat
 
         protected override IEnumerable<Drawable> EffectTargets => Parts;
 
-        public string TooltipText { get; set; }
-
         private class LinkHoverSounds : HoverClickSounds
         {
             private readonly List<Drawable> parts;
diff --git a/osu.Game/Overlays/BeatmapSet/AuthorInfo.cs b/osu.Game/Overlays/BeatmapSet/AuthorInfo.cs
index abe954aa80..7331faa618 100644
--- a/osu.Game/Overlays/BeatmapSet/AuthorInfo.cs
+++ b/osu.Game/Overlays/BeatmapSet/AuthorInfo.cs
@@ -9,8 +9,6 @@ using osu.Game.Graphics.Sprites;
 using osu.Game.Users;
 using osuTK;
 using osuTK.Graphics;
-using osu.Game.Graphics.Containers;
-using osu.Framework.Graphics.Cursor;
 using osu.Framework.Graphics.Effects;
 using osu.Framework.Graphics.Sprites;
 using osu.Game.Graphics;
@@ -129,10 +127,5 @@ namespace osu.Game.Overlays.BeatmapSet
                 };
             }
         }
-
-        private class ClickableArea : OsuClickableContainer, IHasTooltip
-        {
-            public string TooltipText => @"View Profile";
-        }
     }
 }
diff --git a/osu.Game/Overlays/BeatmapSet/Scores/TopScoreUserSection.cs b/osu.Game/Overlays/BeatmapSet/Scores/TopScoreUserSection.cs
index e70bf4c572..89da0fc254 100644
--- a/osu.Game/Overlays/BeatmapSet/Scores/TopScoreUserSection.cs
+++ b/osu.Game/Overlays/BeatmapSet/Scores/TopScoreUserSection.cs
@@ -83,7 +83,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
                                 Origin = Anchor.CentreLeft,
                                 AutoSizeAxes = Axes.Both,
                             },
-                            date = new SpriteText
+                            date = new OsuSpriteText
                             {
                                 Anchor = Anchor.CentreLeft,
                                 Origin = Anchor.CentreLeft,
diff --git a/osu.Game/Overlays/Profile/Header/BottomHeaderContainer.cs b/osu.Game/Overlays/Profile/Header/BottomHeaderContainer.cs
index 633085960b..ffbb9ad218 100644
--- a/osu.Game/Overlays/Profile/Header/BottomHeaderContainer.cs
+++ b/osu.Game/Overlays/Profile/Header/BottomHeaderContainer.cs
@@ -35,14 +35,14 @@ namespace osu.Game.Overlays.Profile.Header
         [BackgroundDependencyLoader]
         private void load(OsuColour colours)
         {
-            iconColour = colours.CommunityUserGrayGreenLighter;
+            iconColour = colours.GreySeafoamLighter;
 
             InternalChildren = new Drawable[]
             {
                 new Box
                 {
                     RelativeSizeAxes = Axes.Both,
-                    Colour = colours.CommunityUserGrayGreenDarker,
+                    Colour = colours.GreySeafoamDark,
                 },
                 new FillFlowContainer
                 {
diff --git a/osu.Game/Overlays/Profile/Header/CentreHeaderContainer.cs b/osu.Game/Overlays/Profile/Header/CentreHeaderContainer.cs
index b441775393..68fd77dd84 100644
--- a/osu.Game/Overlays/Profile/Header/CentreHeaderContainer.cs
+++ b/osu.Game/Overlays/Profile/Header/CentreHeaderContainer.cs
@@ -38,7 +38,7 @@ namespace osu.Game.Overlays.Profile.Header
                 new Box
                 {
                     RelativeSizeAxes = Axes.Both,
-                    Colour = colours.CommunityUserGrayGreenDark
+                    Colour = colours.GreySeafoam
                 },
                 new FillFlowContainer
                 {
diff --git a/osu.Game/Overlays/Profile/Header/Components/ExpandDetailsButton.cs b/osu.Game/Overlays/Profile/Header/Components/ExpandDetailsButton.cs
index 089228b2cd..46d24608ed 100644
--- a/osu.Game/Overlays/Profile/Header/Components/ExpandDetailsButton.cs
+++ b/osu.Game/Overlays/Profile/Header/Components/ExpandDetailsButton.cs
@@ -27,8 +27,8 @@ namespace osu.Game.Overlays.Profile.Header.Components
         [BackgroundDependencyLoader]
         private void load(OsuColour colours)
         {
-            IdleColour = colours.CommunityUserGrayGreen;
-            HoverColour = colours.CommunityUserGrayGreen.Darken(0.2f);
+            IdleColour = colours.GreySeafoamLight;
+            HoverColour = colours.GreySeafoamLight.Darken(0.2f);
 
             Child = icon = new SpriteIcon
             {
diff --git a/osu.Game/Overlays/Profile/Header/Components/ProfileHeaderButton.cs b/osu.Game/Overlays/Profile/Header/Components/ProfileHeaderButton.cs
index 1650f11523..ddcf011277 100644
--- a/osu.Game/Overlays/Profile/Header/Components/ProfileHeaderButton.cs
+++ b/osu.Game/Overlays/Profile/Header/Components/ProfileHeaderButton.cs
@@ -4,7 +4,6 @@
 using System.Collections.Generic;
 using osu.Framework.Graphics;
 using osu.Framework.Graphics.Containers;
-using osu.Framework.Graphics.Cursor;
 using osu.Framework.Graphics.Shapes;
 using osu.Game.Graphics;
 using osu.Game.Graphics.Containers;
@@ -12,10 +11,8 @@ using osuTK.Graphics;
 
 namespace osu.Game.Overlays.Profile.Header.Components
 {
-    public abstract class ProfileHeaderButton : OsuHoverContainer, IHasTooltip
+    public abstract class ProfileHeaderButton : OsuHoverContainer
     {
-        public abstract string TooltipText { get; }
-
         private readonly Box background;
         private readonly Container content;
 
diff --git a/osu.Game/Overlays/Profile/Header/Components/RankGraph.cs b/osu.Game/Overlays/Profile/Header/Components/RankGraph.cs
index 1dabf167e3..85ea2a175a 100644
--- a/osu.Game/Overlays/Profile/Header/Components/RankGraph.cs
+++ b/osu.Game/Overlays/Profile/Header/Components/RankGraph.cs
@@ -154,7 +154,7 @@ namespace osu.Game.Overlays.Profile.Header.Components
             [BackgroundDependencyLoader]
             private void load(OsuColour colours)
             {
-                ballBg.Colour = colours.CommunityUserGrayGreenDarkest;
+                ballBg.Colour = colours.GreySeafoamDarker;
                 movingBall.BorderColour = colours.Yellow;
                 movingBar.Colour = colours.Yellow;
             }
@@ -249,7 +249,7 @@ namespace osu.Game.Overlays.Profile.Header.Components
             [BackgroundDependencyLoader]
             private void load(OsuColour colours)
             {
-                background.Colour = colours.CommunityUserGrayGreenDarker;
+                background.Colour = colours.GreySeafoamDark;
             }
 
             public void Refresh()
diff --git a/osu.Game/Overlays/Profile/Header/Components/SupporterIcon.cs b/osu.Game/Overlays/Profile/Header/Components/SupporterIcon.cs
index 97454d7327..c5e61f68f4 100644
--- a/osu.Game/Overlays/Profile/Header/Components/SupporterIcon.cs
+++ b/osu.Game/Overlays/Profile/Header/Components/SupporterIcon.cs
@@ -80,7 +80,7 @@ namespace osu.Game.Overlays.Profile.Header.Components
         private void load(OsuColour colours)
         {
             background.Colour = colours.Pink;
-            iconContainer.Colour = colours.CommunityUserGrayGreenDark;
+            iconContainer.Colour = colours.GreySeafoam;
         }
     }
 }
diff --git a/osu.Game/Overlays/Profile/Header/DetailHeaderContainer.cs b/osu.Game/Overlays/Profile/Header/DetailHeaderContainer.cs
index e41c90be45..f26cc360a2 100644
--- a/osu.Game/Overlays/Profile/Header/DetailHeaderContainer.cs
+++ b/osu.Game/Overlays/Profile/Header/DetailHeaderContainer.cs
@@ -65,7 +65,7 @@ namespace osu.Game.Overlays.Profile.Header
                 new Box
                 {
                     RelativeSizeAxes = Axes.Both,
-                    Colour = colours.CommunityUserGrayGreenDarkest,
+                    Colour = colours.GreySeafoamDarker,
                 },
                 fillFlow = new FillFlowContainer
                 {
diff --git a/osu.Game/Overlays/Profile/Header/MedalHeaderContainer.cs b/osu.Game/Overlays/Profile/Header/MedalHeaderContainer.cs
index 25d04195b2..67229a80c0 100644
--- a/osu.Game/Overlays/Profile/Header/MedalHeaderContainer.cs
+++ b/osu.Game/Overlays/Profile/Header/MedalHeaderContainer.cs
@@ -34,7 +34,7 @@ namespace osu.Game.Overlays.Profile.Header
                 new Box
                 {
                     RelativeSizeAxes = Axes.Both,
-                    Colour = colours.CommunityUserGrayGreenDarkest,
+                    Colour = colours.GreySeafoamDarker,
                 },
                 new Container //artificial shadow
                 {
diff --git a/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs b/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs
index 2ac7f3cc96..6fe55e2368 100644
--- a/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs
+++ b/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs
@@ -41,7 +41,7 @@ namespace osu.Game.Overlays.Profile.Header
                 new Box
                 {
                     RelativeSizeAxes = Axes.Both,
-                    Colour = colours.CommunityUserGrayGreenDarker,
+                    Colour = colours.GreySeafoamDark,
                 },
                 new FillFlowContainer
                 {
@@ -107,7 +107,7 @@ namespace osu.Game.Overlays.Profile.Header
                                             RelativeSizeAxes = Axes.X,
                                             Height = 1.5f,
                                             Margin = new MarginPadding { Top = 10 },
-                                            Colour = colours.CommunityUserGrayGreenLighter,
+                                            Colour = colours.GreySeafoamLighter,
                                         },
                                         new Container
                                         {
@@ -125,7 +125,7 @@ namespace osu.Game.Overlays.Profile.Header
                                                     Margin = new MarginPadding { Left = 40 },
                                                     Origin = Anchor.CentreLeft,
                                                     Anchor = Anchor.CentreLeft,
-                                                    Colour = colours.CommunityUserGrayGreenLighter,
+                                                    Colour = colours.GreySeafoamLighter,
                                                 }
                                             }
                                         },
diff --git a/osu.Game/Overlays/Profile/ProfileHeader.cs b/osu.Game/Overlays/Profile/ProfileHeader.cs
index 2d8c47b11a..f2ac94b7ff 100644
--- a/osu.Game/Overlays/Profile/ProfileHeader.cs
+++ b/osu.Game/Overlays/Profile/ProfileHeader.cs
@@ -124,7 +124,7 @@ namespace osu.Game.Overlays.Profile
         [BackgroundDependencyLoader]
         private void load(OsuColour colours)
         {
-            infoTabControl.AccentColour = colours.CommunityUserGreen;
+            infoTabControl.AccentColour = colours.Seafoam;
         }
 
         public Bindable<User> User = new Bindable<User>();
@@ -145,7 +145,7 @@ namespace osu.Game.Overlays.Profile
             [BackgroundDependencyLoader]
             private void load(OsuColour colours)
             {
-                AccentColour = colours.CommunityUserGreen;
+                AccentColour = colours.Seafoam;
             }
         }
     }
diff --git a/osu.Game/Overlays/Profile/Sections/BeatmapMetadataContainer.cs b/osu.Game/Overlays/Profile/Sections/BeatmapMetadataContainer.cs
index bb55816880..16326900f1 100644
--- a/osu.Game/Overlays/Profile/Sections/BeatmapMetadataContainer.cs
+++ b/osu.Game/Overlays/Profile/Sections/BeatmapMetadataContainer.cs
@@ -4,7 +4,6 @@
 using osu.Framework.Allocation;
 using osu.Framework.Graphics;
 using osu.Framework.Graphics.Containers;
-using osu.Framework.Graphics.Cursor;
 using osu.Framework.Localisation;
 using osu.Game.Beatmaps;
 using osu.Game.Graphics;
@@ -16,7 +15,7 @@ namespace osu.Game.Overlays.Profile.Sections
     /// <summary>
     /// Display artist/title/mapper information, commonly used as the left portion of a profile or score display row (see <see cref="DrawableProfileRow"/>).
     /// </summary>
-    public class BeatmapMetadataContainer : OsuHoverContainer, IHasTooltip
+    public class BeatmapMetadataContainer : OsuHoverContainer
     {
         private readonly BeatmapInfo beatmap;
 
@@ -27,8 +26,6 @@ namespace osu.Game.Overlays.Profile.Sections
             TooltipText = $"{beatmap.Metadata.Artist} - {beatmap.Metadata.Title}";
         }
 
-        public string TooltipText { get; }
-
         [BackgroundDependencyLoader(true)]
         private void load(BeatmapSetOverlay beatmapSetOverlay)
         {
diff --git a/osu.Game/Users/Avatar.cs b/osu.Game/Users/Avatar.cs
index 3df5957ff9..8937f94768 100644
--- a/osu.Game/Users/Avatar.cs
+++ b/osu.Game/Users/Avatar.cs
@@ -6,7 +6,6 @@ using osu.Framework.Allocation;
 using osu.Framework.Bindables;
 using osu.Framework.Graphics;
 using osu.Framework.Graphics.Containers;
-using osu.Framework.Graphics.Cursor;
 using osu.Framework.Graphics.Sprites;
 using osu.Framework.Graphics.Textures;
 using osu.Framework.Input.Events;
@@ -72,9 +71,9 @@ namespace osu.Game.Users
                 game?.ShowUser(user.Id);
         }
 
-        private class ClickableArea : OsuClickableContainer, IHasTooltip
+        private class ClickableArea : OsuClickableContainer
         {
-            public string TooltipText => Enabled.Value ? @"View Profile" : null;
+            public override string TooltipText => Enabled.Value ? @"View Profile" : null;
 
             protected override bool OnClick(ClickEvent e)
             {