diff --git a/osu.Android.props b/osu.Android.props
index f623a92ade..1a63b893a1 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -51,7 +51,7 @@
     <Reference Include="Java.Interop" />
   </ItemGroup>
   <ItemGroup>
-    <PackageReference Include="ppy.osu.Game.Resources" Version="2020.304.0" />
+    <PackageReference Include="ppy.osu.Game.Resources" Version="2020.315.0" />
     <PackageReference Include="ppy.osu.Framework.Android" Version="2020.312.0" />
   </ItemGroup>
 </Project>
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs
index fbbe00bb6c..fe0d512166 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs
@@ -23,7 +23,7 @@ namespace osu.Game.Rulesets.Catch.Tests
         [BackgroundDependencyLoader]
         private void load()
         {
-            SetContents(() => new CatcherArea.Catcher
+            SetContents(() => new Catcher
             {
                 RelativePositionAxes = Axes.None,
                 Anchor = Anchor.Centre,
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjects.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjects.cs
index 304c7e3854..df5494aab0 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjects.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjects.cs
@@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Catch.Tests
     {
         public override IReadOnlyList<Type> RequiredTypes => new[]
         {
-            typeof(CatcherArea.Catcher),
+            typeof(Catcher),
             typeof(DrawableCatchRuleset),
             typeof(DrawableFruit),
             typeof(DrawableJuiceStream),
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs
index 6f0d8f0a3a..49ff9df4d7 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs
@@ -45,7 +45,7 @@ namespace osu.Game.Rulesets.Catch.Tests
             }
         }
 
-        private CatcherArea.Catcher getCatcher() => Player.ChildrenOfType<CatcherArea>().First().MovableCatcher;
+        private Catcher getCatcher() => Player.ChildrenOfType<CatcherArea>().First().MovableCatcher;
 
         protected override IBeatmap CreateBeatmap(RulesetInfo ruleset)
         {
diff --git a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs
index 986dc9dbb9..7c81bcdf0c 100644
--- a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs
+++ b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs
@@ -227,7 +227,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
                 int thisDirection = nextObject.X > currentObject.X ? 1 : -1;
                 double timeToNext = nextObject.StartTime - currentObject.StartTime - 1000f / 60f / 4; // 1/4th of a frame of grace time, taken from osu-stable
                 double distanceToNext = Math.Abs(nextObject.X - currentObject.X) - (lastDirection == thisDirection ? lastExcess : halfCatcherWidth);
-                float distanceToHyper = (float)(timeToNext * CatcherArea.Catcher.BASE_SPEED - distanceToNext);
+                float distanceToHyper = (float)(timeToNext * Catcher.BASE_SPEED - distanceToNext);
 
                 if (distanceToHyper < 0)
                 {
diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs
index 44e1a8e5cc..5880a227c2 100644
--- a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs
+++ b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs
@@ -23,6 +23,8 @@ namespace osu.Game.Rulesets.Catch.Difficulty
 
         protected override int SectionLength => 750;
 
+        private float halfCatcherWidth;
+
         public CatchDifficultyCalculator(Ruleset ruleset, WorkingBeatmap beatmap)
             : base(ruleset, beatmap)
         {
@@ -48,14 +50,6 @@ namespace osu.Game.Rulesets.Catch.Difficulty
 
         protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate)
         {
-            float halfCatchWidth;
-
-            using (var catcher = new CatcherArea.Catcher(beatmap.BeatmapInfo.BaseDifficulty))
-            {
-                halfCatchWidth = catcher.CatchWidth * 0.5f;
-                halfCatchWidth *= 0.8f; // We're only using 80% of the catcher's width to simulate imperfect gameplay.
-            }
-
             CatchHitObject lastObject = null;
 
             // In 2B beatmaps, it is possible that a normal Fruit is placed in the middle of a JuiceStream.
@@ -69,16 +63,25 @@ namespace osu.Game.Rulesets.Catch.Difficulty
                     continue;
 
                 if (lastObject != null)
-                    yield return new CatchDifficultyHitObject(hitObject, lastObject, clockRate, halfCatchWidth);
+                    yield return new CatchDifficultyHitObject(hitObject, lastObject, clockRate, halfCatcherWidth);
 
                 lastObject = hitObject;
             }
         }
 
-        protected override Skill[] CreateSkills(IBeatmap beatmap) => new Skill[]
+        protected override Skill[] CreateSkills(IBeatmap beatmap)
         {
-            new Movement(),
-        };
+            using (var catcher = new Catcher(beatmap.BeatmapInfo.BaseDifficulty))
+            {
+                halfCatcherWidth = catcher.CatchWidth * 0.5f;
+                halfCatcherWidth *= 0.8f; // We're only using 80% of the catcher's width to simulate imperfect gameplay.
+            }
+
+            return new Skill[]
+            {
+                new Movement(halfCatcherWidth),
+            };
+        }
 
         protected override Mod[] DifficultyAdjustmentMods => new Mod[]
         {
diff --git a/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs b/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs
index 7cd569035b..fd164907e0 100644
--- a/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs
+++ b/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs
@@ -20,9 +20,16 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills
 
         protected override double DecayWeight => 0.94;
 
+        protected readonly float HalfCatcherWidth;
+
         private float? lastPlayerPosition;
         private float lastDistanceMoved;
 
+        public Movement(float halfCatcherWidth)
+        {
+            HalfCatcherWidth = halfCatcherWidth;
+        }
+
         protected override double StrainValueOf(DifficultyHitObject current)
         {
             var catchCurrent = (CatchDifficultyHitObject)current;
diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModRelax.cs b/osu.Game.Rulesets.Catch/Mods/CatchModRelax.cs
index 4c72b9fd3e..1ef235f764 100644
--- a/osu.Game.Rulesets.Catch/Mods/CatchModRelax.cs
+++ b/osu.Game.Rulesets.Catch/Mods/CatchModRelax.cs
@@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Catch.Mods
 
         private class MouseInputHelper : Drawable, IKeyBindingHandler<CatchAction>, IRequireHighFrequencyMousePosition
         {
-            private readonly CatcherArea.Catcher catcher;
+            private readonly Catcher catcher;
 
             public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true;
 
diff --git a/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs b/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs
index 4649dcae90..b90b5812a6 100644
--- a/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs
+++ b/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs
@@ -32,7 +32,7 @@ namespace osu.Game.Rulesets.Catch.Replays
         public override Replay Generate()
         {
             // todo: add support for HT DT
-            const double dash_speed = CatcherArea.Catcher.BASE_SPEED;
+            const double dash_speed = Catcher.BASE_SPEED;
             const double movement_speed = dash_speed / 2;
             float lastPosition = 0.5f;
             double lastTime = 0;
diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs
new file mode 100644
index 0000000000..a3dc58bc19
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs
@@ -0,0 +1,460 @@
+// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Linq;
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Animations;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Input.Bindings;
+using osu.Framework.Utils;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Catch.Objects;
+using osu.Game.Rulesets.Catch.Objects.Drawables;
+using osu.Game.Rulesets.Objects.Drawables;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Catch.UI
+{
+    public class Catcher : Container, IKeyBindingHandler<CatchAction>
+    {
+        /// <summary>
+        /// Whether we are hyper-dashing or not.
+        /// </summary>
+        public bool HyperDashing => hyperDashModifier != 1;
+
+        /// <summary>
+        /// The relative space to cover in 1 millisecond. based on 1 game pixel per millisecond as in osu-stable.
+        /// </summary>
+        public const double BASE_SPEED = 1.0 / 512;
+
+        public Container ExplodingFruitTarget;
+
+        public Container AdditiveTarget;
+
+        public CatcherAnimationState CurrentState { get; private set; }
+
+        /// <summary>
+        /// Width of the area that can be used to attempt catches during gameplay.
+        /// </summary>
+        internal float CatchWidth => CatcherArea.CATCHER_SIZE * Math.Abs(Scale.X);
+
+        protected bool Dashing
+        {
+            get => dashing;
+            set
+            {
+                if (value == dashing) return;
+
+                dashing = value;
+
+                Trail |= dashing;
+            }
+        }
+
+        /// <summary>
+        /// Activate or deactive the trail. Will be automatically deactivated when conditions to keep the trail displayed are no longer met.
+        /// </summary>
+        protected bool Trail
+        {
+            get => trail;
+            set
+            {
+                if (value == trail) return;
+
+                trail = value;
+
+                if (Trail)
+                    beginTrail();
+            }
+        }
+
+        private Container<DrawableHitObject> caughtFruit;
+
+        private CatcherSprite catcherIdle;
+        private CatcherSprite catcherKiai;
+        private CatcherSprite catcherFail;
+
+        private int currentDirection;
+
+        private bool dashing;
+
+        private bool trail;
+
+        private double hyperDashModifier = 1;
+        private int hyperDashDirection;
+        private float hyperDashTargetPosition;
+
+        public Catcher(BeatmapDifficulty difficulty = null)
+        {
+            RelativePositionAxes = Axes.X;
+            X = 0.5f;
+
+            Origin = Anchor.TopCentre;
+
+            Size = new Vector2(CatcherArea.CATCHER_SIZE);
+            if (difficulty != null)
+                Scale = new Vector2(1.0f - 0.7f * (difficulty.CircleSize - 5) / 5);
+        }
+
+        [BackgroundDependencyLoader]
+        private void load()
+        {
+            Children = new Drawable[]
+            {
+                caughtFruit = new Container<DrawableHitObject>
+                {
+                    Anchor = Anchor.TopCentre,
+                    Origin = Anchor.BottomCentre,
+                },
+                catcherIdle = new CatcherSprite(CatcherAnimationState.Idle)
+                {
+                    Anchor = Anchor.TopCentre,
+                    Alpha = 0,
+                },
+                catcherKiai = new CatcherSprite(CatcherAnimationState.Kiai)
+                {
+                    Anchor = Anchor.TopCentre,
+                    Alpha = 0,
+                },
+                catcherFail = new CatcherSprite(CatcherAnimationState.Fail)
+                {
+                    Anchor = Anchor.TopCentre,
+                    Alpha = 0,
+                }
+            };
+
+            updateCatcher();
+        }
+
+        /// <summary>
+        /// Add a caught fruit to the catcher's stack.
+        /// </summary>
+        /// <param name="fruit">The fruit that was caught.</param>
+        public void PlaceOnPlate(DrawableCatchHitObject fruit)
+        {
+            var ourRadius = fruit.DisplayRadius;
+            float theirRadius = 0;
+
+            const float allowance = 6;
+
+            while (caughtFruit.Any(f =>
+                f.LifetimeEnd == double.MaxValue &&
+                Vector2Extensions.Distance(f.Position, fruit.Position) < (ourRadius + (theirRadius = f.DrawSize.X / 2 * f.Scale.X)) / (allowance / 2)))
+            {
+                var diff = (ourRadius + theirRadius) / allowance;
+                fruit.X += (RNG.NextSingle() - 0.5f) * 2 * diff;
+                fruit.Y -= RNG.NextSingle() * diff;
+            }
+
+            fruit.X = Math.Clamp(fruit.X, -CatcherArea.CATCHER_SIZE / 2, CatcherArea.CATCHER_SIZE / 2);
+
+            caughtFruit.Add(fruit);
+
+            Add(new HitExplosion(fruit)
+            {
+                X = fruit.X,
+                Scale = new Vector2(fruit.HitObject.Scale)
+            });
+        }
+
+        /// <summary>
+        /// Let the catcher attempt to catch a fruit.
+        /// </summary>
+        /// <param name="fruit">The fruit to catch.</param>
+        /// <returns>Whether the catch is possible.</returns>
+        public bool AttemptCatch(CatchHitObject fruit)
+        {
+            var halfCatchWidth = CatchWidth * 0.5f;
+
+            // this stuff wil disappear once we move fruit to non-relative coordinate space in the future.
+            var catchObjectPosition = fruit.X * CatchPlayfield.BASE_WIDTH;
+            var catcherPosition = Position.X * CatchPlayfield.BASE_WIDTH;
+
+            var validCatch =
+                catchObjectPosition >= catcherPosition - halfCatchWidth &&
+                catchObjectPosition <= catcherPosition + halfCatchWidth;
+
+            // only update hyperdash state if we are catching a fruit.
+            // exceptions are Droplets and JuiceStreams.
+            if (!(fruit is Fruit)) return validCatch;
+
+            if (validCatch && fruit.HyperDash)
+            {
+                var target = fruit.HyperDashTarget;
+                var timeDifference = target.StartTime - fruit.StartTime;
+                double positionDifference = target.X * CatchPlayfield.BASE_WIDTH - catcherPosition;
+                var velocity = positionDifference / Math.Max(1.0, timeDifference - 1000.0 / 60.0);
+
+                SetHyperDashState(Math.Abs(velocity), target.X);
+            }
+            else
+                SetHyperDashState();
+
+            if (validCatch)
+                updateState(fruit.Kiai ? CatcherAnimationState.Kiai : CatcherAnimationState.Idle);
+            else if (!(fruit is Banana))
+                updateState(CatcherAnimationState.Fail);
+
+            return validCatch;
+        }
+
+        /// <summary>
+        /// Set hyper-dash state.
+        /// </summary>
+        /// <param name="modifier">The speed multiplier. If this is less or equals to 1, this catcher will be non-hyper-dashing state.</param>
+        /// <param name="targetPosition">When this catcher crosses this position, this catcher ends hyper-dashing.</param>
+        public void SetHyperDashState(double modifier = 1, float targetPosition = -1)
+        {
+            const float hyper_dash_transition_length = 180;
+
+            var wasHyperDashing = HyperDashing;
+
+            if (modifier <= 1 || X == targetPosition)
+            {
+                hyperDashModifier = 1;
+                hyperDashDirection = 0;
+
+                if (wasHyperDashing)
+                {
+                    this.FadeColour(Color4.White, hyper_dash_transition_length, Easing.OutQuint);
+                    this.FadeTo(1, hyper_dash_transition_length, Easing.OutQuint);
+                    Trail &= Dashing;
+                }
+            }
+            else
+            {
+                hyperDashModifier = modifier;
+                hyperDashDirection = Math.Sign(targetPosition - X);
+                hyperDashTargetPosition = targetPosition;
+
+                if (!wasHyperDashing)
+                {
+                    this.FadeColour(Color4.OrangeRed, hyper_dash_transition_length, Easing.OutQuint);
+                    this.FadeTo(0.2f, hyper_dash_transition_length, Easing.OutQuint);
+                    Trail = true;
+
+                    var hyperDashEndGlow = createAdditiveSprite(true);
+
+                    hyperDashEndGlow.MoveToOffset(new Vector2(0, -20), 1200, Easing.In);
+                    hyperDashEndGlow.ScaleTo(hyperDashEndGlow.Scale * 0.9f).ScaleTo(hyperDashEndGlow.Scale * 1.2f, 1200, Easing.In);
+                    hyperDashEndGlow.FadeOut(1200);
+                    hyperDashEndGlow.Expire(true);
+                }
+            }
+        }
+
+        public bool OnPressed(CatchAction action)
+        {
+            switch (action)
+            {
+                case CatchAction.MoveLeft:
+                    currentDirection--;
+                    return true;
+
+                case CatchAction.MoveRight:
+                    currentDirection++;
+                    return true;
+
+                case CatchAction.Dash:
+                    Dashing = true;
+                    return true;
+            }
+
+            return false;
+        }
+
+        public void OnReleased(CatchAction action)
+        {
+            switch (action)
+            {
+                case CatchAction.MoveLeft:
+                    currentDirection++;
+                    break;
+
+                case CatchAction.MoveRight:
+                    currentDirection--;
+                    break;
+
+                case CatchAction.Dash:
+                    Dashing = false;
+                    break;
+            }
+        }
+
+        public void UpdatePosition(float position)
+        {
+            position = Math.Clamp(position, 0, 1);
+
+            if (position == X)
+                return;
+
+            Scale = new Vector2(Math.Abs(Scale.X) * (position > X ? 1 : -1), Scale.Y);
+            X = position;
+        }
+
+        /// <summary>
+        /// Drop any fruit off the plate.
+        /// </summary>
+        public void Drop()
+        {
+            foreach (var f in caughtFruit.ToArray())
+                Drop(f);
+        }
+
+        /// <summary>
+        /// Explode any fruit off the plate.
+        /// </summary>
+        public void Explode()
+        {
+            foreach (var f in caughtFruit.ToArray())
+                Explode(f);
+        }
+
+        public void Drop(DrawableHitObject fruit)
+        {
+            removeFromPlateWithTransform(fruit, f =>
+            {
+                f.MoveToY(f.Y + 75, 750, Easing.InSine);
+                f.FadeOut(750);
+            });
+        }
+
+        public void Explode(DrawableHitObject fruit)
+        {
+            var originalX = fruit.X * Scale.X;
+
+            removeFromPlateWithTransform(fruit, f =>
+            {
+                f.MoveToY(f.Y - 50, 250, Easing.OutSine).Then().MoveToY(f.Y + 50, 500, Easing.InSine);
+                f.MoveToX(f.X + originalX * 6, 1000);
+                f.FadeOut(750);
+            });
+        }
+
+        protected override void Update()
+        {
+            base.Update();
+
+            if (currentDirection == 0) return;
+
+            var direction = Math.Sign(currentDirection);
+
+            var dashModifier = Dashing ? 1 : 0.5;
+            var speed = BASE_SPEED * dashModifier * hyperDashModifier;
+
+            UpdatePosition((float)(X + direction * Clock.ElapsedFrameTime * speed));
+
+            // Correct overshooting.
+            if ((hyperDashDirection > 0 && hyperDashTargetPosition < X) ||
+                (hyperDashDirection < 0 && hyperDashTargetPosition > X))
+            {
+                X = hyperDashTargetPosition;
+                SetHyperDashState();
+            }
+        }
+
+        private void updateCatcher()
+        {
+            catcherIdle.Hide();
+            catcherKiai.Hide();
+            catcherFail.Hide();
+
+            CatcherSprite current;
+
+            switch (CurrentState)
+            {
+                default:
+                    current = catcherIdle;
+                    break;
+
+                case CatcherAnimationState.Fail:
+                    current = catcherFail;
+                    break;
+
+                case CatcherAnimationState.Kiai:
+                    current = catcherKiai;
+                    break;
+            }
+
+            current.Show();
+            (current.Drawable as IAnimation)?.GotoFrame(0);
+        }
+
+        private void beginTrail()
+        {
+            Trail &= dashing || HyperDashing;
+            Trail &= AdditiveTarget != null;
+
+            if (!Trail) return;
+
+            var additive = createAdditiveSprite(HyperDashing);
+
+            additive.FadeTo(0.4f).FadeOut(800, Easing.OutQuint);
+            additive.Expire(true);
+
+            Scheduler.AddDelayed(beginTrail, HyperDashing ? 25 : 50);
+        }
+
+        private Drawable createAdditiveSprite(bool hyperDash)
+        {
+            var additive = createCatcherSprite();
+
+            additive.Anchor = Anchor;
+            additive.Scale = Scale;
+            additive.Colour = hyperDash ? Color4.Red : Color4.White;
+            additive.Blending = BlendingParameters.Additive;
+            additive.RelativePositionAxes = RelativePositionAxes;
+            additive.Position = Position;
+
+            AdditiveTarget.Add(additive);
+
+            return additive;
+        }
+
+        private Drawable createCatcherSprite()
+        {
+            return new CatcherSprite(CurrentState);
+        }
+
+        private void updateState(CatcherAnimationState state)
+        {
+            if (CurrentState == state)
+                return;
+
+            CurrentState = state;
+            updateCatcher();
+        }
+
+        private void removeFromPlateWithTransform(DrawableHitObject fruit, Action<DrawableHitObject> action)
+        {
+            if (ExplodingFruitTarget != null)
+            {
+                fruit.Anchor = Anchor.TopLeft;
+                fruit.Position = caughtFruit.ToSpaceOfOtherDrawable(fruit.DrawPosition, ExplodingFruitTarget);
+
+                if (!caughtFruit.Remove(fruit))
+                    // we may have already been removed by a previous operation (due to the weird OnLoadComplete scheduling).
+                    // this avoids a crash on potentially attempting to Add a fruit to ExplodingFruitTarget twice.
+                    return;
+
+                ExplodingFruitTarget.Add(fruit);
+            }
+
+            var actionTime = Clock.CurrentTime;
+
+            fruit.ApplyCustomUpdateState += onFruitOnApplyCustomUpdateState;
+            onFruitOnApplyCustomUpdateState(fruit, fruit.State.Value);
+
+            void onFruitOnApplyCustomUpdateState(DrawableHitObject o, ArmedState state)
+            {
+                using (fruit.BeginAbsoluteSequence(actionTime))
+                    action(fruit);
+
+                fruit.Expire();
+            }
+        }
+    }
+}
diff --git a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs
index 2394110165..e0d9ff759d 100644
--- a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs
+++ b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs
@@ -2,15 +2,8 @@
 // See the LICENCE file in the repository root for full licence text.
 
 using System;
-using System.Linq;
-using osu.Framework.Allocation;
-using osu.Framework.Extensions.Color4Extensions;
 using osu.Framework.Graphics;
-using osu.Framework.Graphics.Animations;
 using osu.Framework.Graphics.Containers;
-using osu.Framework.Graphics.Effects;
-using osu.Framework.Input.Bindings;
-using osu.Framework.Utils;
 using osu.Game.Beatmaps;
 using osu.Game.Rulesets.Catch.Judgements;
 using osu.Game.Rulesets.Catch.Objects;
@@ -20,7 +13,6 @@ using osu.Game.Rulesets.Judgements;
 using osu.Game.Rulesets.Objects.Drawables;
 using osu.Game.Rulesets.UI;
 using osuTK;
-using osuTK.Graphics;
 
 namespace osu.Game.Rulesets.Catch.UI
 {
@@ -28,8 +20,6 @@ namespace osu.Game.Rulesets.Catch.UI
     {
         public const float CATCHER_SIZE = 106.75f;
 
-        protected internal readonly Catcher MovableCatcher;
-
         public Func<CatchHitObject, DrawableHitObject<CatchHitObject>> CreateDrawableRepresentation;
 
         public Container ExplodingFruitTarget
@@ -37,6 +27,8 @@ namespace osu.Game.Rulesets.Catch.UI
             set => MovableCatcher.ExplodingFruitTarget = value;
         }
 
+        private DrawableCatchHitObject lastPlateableFruit;
+
         public CatcherArea(BeatmapDifficulty difficulty = null)
         {
             RelativeSizeAxes = Axes.X;
@@ -47,7 +39,10 @@ namespace osu.Game.Rulesets.Catch.UI
             };
         }
 
-        private DrawableCatchHitObject lastPlateableFruit;
+        public static float GetCatcherSize(BeatmapDifficulty difficulty)
+        {
+            return CATCHER_SIZE / CatchPlayfield.BASE_WIDTH * (1.0f - 0.7f * (difficulty.CircleSize - 5) / 5);
+        }
 
         public void OnResult(DrawableCatchHitObject fruit, JudgementResult result)
         {
@@ -100,6 +95,15 @@ namespace osu.Game.Rulesets.Catch.UI
             }
         }
 
+        public void OnReleased(CatchAction action)
+        {
+        }
+
+        public bool AttemptCatch(CatchHitObject obj)
+        {
+            return MovableCatcher.AttemptCatch(obj);
+        }
+
         protected override void UpdateAfterChildren()
         {
             base.UpdateAfterChildren();
@@ -110,559 +114,6 @@ namespace osu.Game.Rulesets.Catch.UI
                 MovableCatcher.X = state.CatcherX.Value;
         }
 
-        public void OnReleased(CatchAction action)
-        {
-        }
-
-        public bool AttemptCatch(CatchHitObject obj) => MovableCatcher.AttemptCatch(obj);
-
-        public static float GetCatcherSize(BeatmapDifficulty difficulty)
-        {
-            return CATCHER_SIZE / CatchPlayfield.BASE_WIDTH * (1.0f - 0.7f * (difficulty.CircleSize - 5) / 5);
-        }
-
-        public class Catcher : Container, IKeyBindingHandler<CatchAction>
-        {
-            /// <summary>
-            /// Width of the area that can be used to attempt catches during gameplay.
-            /// </summary>
-            internal float CatchWidth => CATCHER_SIZE * Math.Abs(Scale.X);
-
-            private Container<DrawableHitObject> caughtFruit;
-
-            public Container ExplodingFruitTarget;
-
-            public Container AdditiveTarget;
-
-            public Catcher(BeatmapDifficulty difficulty = null)
-            {
-                RelativePositionAxes = Axes.X;
-                X = 0.5f;
-
-                Origin = Anchor.TopCentre;
-
-                Size = new Vector2(CATCHER_SIZE);
-                if (difficulty != null)
-                    Scale = new Vector2(1.0f - 0.7f * (difficulty.CircleSize - 5) / 5);
-            }
-
-            [BackgroundDependencyLoader]
-            private void load()
-            {
-                Children = new Drawable[]
-                {
-                    caughtFruit = new Container<DrawableHitObject>
-                    {
-                        Anchor = Anchor.TopCentre,
-                        Origin = Anchor.BottomCentre,
-                    },
-                    catcherIdle = new CatcherSprite(CatcherAnimationState.Idle)
-                    {
-                        Anchor = Anchor.TopCentre,
-                        Alpha = 0,
-                    },
-                    catcherKiai = new CatcherSprite(CatcherAnimationState.Kiai)
-                    {
-                        Anchor = Anchor.TopCentre,
-                        Alpha = 0,
-                    },
-                    catcherFail = new CatcherSprite(CatcherAnimationState.Fail)
-                    {
-                        Anchor = Anchor.TopCentre,
-                        Alpha = 0,
-                    }
-                };
-
-                updateCatcher();
-            }
-
-            private CatcherSprite catcherIdle;
-            private CatcherSprite catcherKiai;
-            private CatcherSprite catcherFail;
-
-            private void updateCatcher()
-            {
-                catcherIdle.Hide();
-                catcherKiai.Hide();
-                catcherFail.Hide();
-
-                CatcherSprite current;
-
-                switch (CurrentState)
-                {
-                    default:
-                        current = catcherIdle;
-                        break;
-
-                    case CatcherAnimationState.Fail:
-                        current = catcherFail;
-                        break;
-
-                    case CatcherAnimationState.Kiai:
-                        current = catcherKiai;
-                        break;
-                }
-
-                current.Show();
-                (current.Drawable as IAnimation)?.GotoFrame(0);
-            }
-
-            private int currentDirection;
-
-            private bool dashing;
-
-            protected bool Dashing
-            {
-                get => dashing;
-                set
-                {
-                    if (value == dashing) return;
-
-                    dashing = value;
-
-                    Trail |= dashing;
-                }
-            }
-
-            private bool trail;
-
-            /// <summary>
-            /// Activate or deactive the trail. Will be automatically deactivated when conditions to keep the trail displayed are no longer met.
-            /// </summary>
-            protected bool Trail
-            {
-                get => trail;
-                set
-                {
-                    if (value == trail) return;
-
-                    trail = value;
-
-                    if (Trail)
-                        beginTrail();
-                }
-            }
-
-            private void beginTrail()
-            {
-                Trail &= dashing || HyperDashing;
-                Trail &= AdditiveTarget != null;
-
-                if (!Trail) return;
-
-                var additive = createAdditiveSprite(HyperDashing);
-
-                additive.FadeTo(0.4f).FadeOut(800, Easing.OutQuint);
-                additive.Expire(true);
-
-                Scheduler.AddDelayed(beginTrail, HyperDashing ? 25 : 50);
-            }
-
-            private Drawable createAdditiveSprite(bool hyperDash)
-            {
-                var additive = createCatcherSprite();
-
-                additive.Anchor = Anchor;
-                additive.Scale = Scale;
-                additive.Colour = hyperDash ? Color4.Red : Color4.White;
-                additive.Blending = BlendingParameters.Additive;
-                additive.RelativePositionAxes = RelativePositionAxes;
-                additive.Position = Position;
-
-                AdditiveTarget.Add(additive);
-
-                return additive;
-            }
-
-            private Drawable createCatcherSprite() => new CatcherSprite(CurrentState);
-
-            /// <summary>
-            /// Add a caught fruit to the catcher's stack.
-            /// </summary>
-            /// <param name="fruit">The fruit that was caught.</param>
-            public void PlaceOnPlate(DrawableCatchHitObject fruit)
-            {
-                float ourRadius = fruit.DisplayRadius;
-                float theirRadius = 0;
-
-                const float allowance = 6;
-
-                while (caughtFruit.Any(f =>
-                    f.LifetimeEnd == double.MaxValue &&
-                    Vector2Extensions.Distance(f.Position, fruit.Position) < (ourRadius + (theirRadius = f.DrawSize.X / 2 * f.Scale.X)) / (allowance / 2)))
-                {
-                    float diff = (ourRadius + theirRadius) / allowance;
-                    fruit.X += (RNG.NextSingle() - 0.5f) * 2 * diff;
-                    fruit.Y -= RNG.NextSingle() * diff;
-                }
-
-                fruit.X = Math.Clamp(fruit.X, -CATCHER_SIZE / 2, CATCHER_SIZE / 2);
-
-                caughtFruit.Add(fruit);
-
-                Add(new HitExplosion(fruit)
-                {
-                    X = fruit.X,
-                    Scale = new Vector2(fruit.HitObject.Scale)
-                });
-            }
-
-            /// <summary>
-            /// Let the catcher attempt to catch a fruit.
-            /// </summary>
-            /// <param name="fruit">The fruit to catch.</param>
-            /// <returns>Whether the catch is possible.</returns>
-            public bool AttemptCatch(CatchHitObject fruit)
-            {
-                float halfCatchWidth = CatchWidth * 0.5f;
-
-                // this stuff wil disappear once we move fruit to non-relative coordinate space in the future.
-                var catchObjectPosition = fruit.X * CatchPlayfield.BASE_WIDTH;
-                var catcherPosition = Position.X * CatchPlayfield.BASE_WIDTH;
-
-                var validCatch =
-                    catchObjectPosition >= catcherPosition - halfCatchWidth &&
-                    catchObjectPosition <= catcherPosition + halfCatchWidth;
-
-                // only update hyperdash state if we are catching a fruit.
-                // exceptions are Droplets and JuiceStreams.
-                if (!(fruit is Fruit)) return validCatch;
-
-                if (validCatch && fruit.HyperDash)
-                {
-                    var target = fruit.HyperDashTarget;
-                    double timeDifference = target.StartTime - fruit.StartTime;
-                    double positionDifference = target.X * CatchPlayfield.BASE_WIDTH - catcherPosition;
-                    double velocity = positionDifference / Math.Max(1.0, timeDifference - 1000.0 / 60.0);
-
-                    SetHyperDashState(Math.Abs(velocity), target.X);
-                }
-                else
-                {
-                    SetHyperDashState();
-                }
-
-                if (validCatch)
-                    updateState(fruit.Kiai ? CatcherAnimationState.Kiai : CatcherAnimationState.Idle);
-                else if (!(fruit is Banana))
-                    updateState(CatcherAnimationState.Fail);
-
-                return validCatch;
-            }
-
-            private void updateState(CatcherAnimationState state)
-            {
-                if (CurrentState == state)
-                    return;
-
-                CurrentState = state;
-                updateCatcher();
-            }
-
-            public CatcherAnimationState CurrentState { get; private set; }
-
-            private double hyperDashModifier = 1;
-            private int hyperDashDirection;
-            private float hyperDashTargetPosition;
-
-            /// <summary>
-            /// Whether we are hyper-dashing or not.
-            /// </summary>
-            public bool HyperDashing => hyperDashModifier != 1;
-
-            /// <summary>
-            /// Set hyper-dash state.
-            /// </summary>
-            /// <param name="modifier">The speed multiplier. If this is less or equals to 1, this catcher will be non-hyper-dashing state.</param>
-            /// <param name="targetPosition">When this catcher crosses this position, this catcher ends hyper-dashing.</param>
-            public void SetHyperDashState(double modifier = 1, float targetPosition = -1)
-            {
-                const float hyper_dash_transition_length = 180;
-
-                bool wasHyperDashing = HyperDashing;
-
-                if (modifier <= 1 || X == targetPosition)
-                {
-                    hyperDashModifier = 1;
-                    hyperDashDirection = 0;
-
-                    if (wasHyperDashing)
-                    {
-                        this.FadeColour(Color4.White, hyper_dash_transition_length, Easing.OutQuint);
-                        this.FadeTo(1, hyper_dash_transition_length, Easing.OutQuint);
-                        Trail &= Dashing;
-                    }
-                }
-                else
-                {
-                    hyperDashModifier = modifier;
-                    hyperDashDirection = Math.Sign(targetPosition - X);
-                    hyperDashTargetPosition = targetPosition;
-
-                    if (!wasHyperDashing)
-                    {
-                        this.FadeColour(Color4.OrangeRed, hyper_dash_transition_length, Easing.OutQuint);
-                        this.FadeTo(0.2f, hyper_dash_transition_length, Easing.OutQuint);
-                        Trail = true;
-
-                        var hyperDashEndGlow = createAdditiveSprite(true);
-
-                        hyperDashEndGlow.MoveToOffset(new Vector2(0, -20), 1200, Easing.In);
-                        hyperDashEndGlow.ScaleTo(hyperDashEndGlow.Scale * 0.9f).ScaleTo(hyperDashEndGlow.Scale * 1.2f, 1200, Easing.In);
-                        hyperDashEndGlow.FadeOut(1200);
-                        hyperDashEndGlow.Expire(true);
-                    }
-                }
-            }
-
-            public bool OnPressed(CatchAction action)
-            {
-                switch (action)
-                {
-                    case CatchAction.MoveLeft:
-                        currentDirection--;
-                        return true;
-
-                    case CatchAction.MoveRight:
-                        currentDirection++;
-                        return true;
-
-                    case CatchAction.Dash:
-                        Dashing = true;
-                        return true;
-                }
-
-                return false;
-            }
-
-            public void OnReleased(CatchAction action)
-            {
-                switch (action)
-                {
-                    case CatchAction.MoveLeft:
-                        currentDirection++;
-                        break;
-
-                    case CatchAction.MoveRight:
-                        currentDirection--;
-                        break;
-
-                    case CatchAction.Dash:
-                        Dashing = false;
-                        break;
-                }
-            }
-
-            /// <summary>
-            /// The relative space to cover in 1 millisecond. based on 1 game pixel per millisecond as in osu-stable.
-            /// </summary>
-            public const double BASE_SPEED = 1.0 / 512;
-
-            protected override void Update()
-            {
-                base.Update();
-
-                if (currentDirection == 0) return;
-
-                var direction = Math.Sign(currentDirection);
-
-                double dashModifier = Dashing ? 1 : 0.5;
-                double speed = BASE_SPEED * dashModifier * hyperDashModifier;
-
-                UpdatePosition((float)(X + direction * Clock.ElapsedFrameTime * speed));
-
-                // Correct overshooting.
-                if ((hyperDashDirection > 0 && hyperDashTargetPosition < X) ||
-                    (hyperDashDirection < 0 && hyperDashTargetPosition > X))
-                {
-                    X = hyperDashTargetPosition;
-                    SetHyperDashState();
-                }
-            }
-
-            public void UpdatePosition(float position)
-            {
-                position = Math.Clamp(position, 0, 1);
-
-                if (position == X)
-                    return;
-
-                Scale = new Vector2(Math.Abs(Scale.X) * (position > X ? 1 : -1), Scale.Y);
-                X = position;
-            }
-
-            /// <summary>
-            /// Drop any fruit off the plate.
-            /// </summary>
-            public void Drop()
-            {
-                foreach (var f in caughtFruit.ToArray())
-                    Drop(f);
-            }
-
-            /// <summary>
-            /// Explode any fruit off the plate.
-            /// </summary>
-            public void Explode()
-            {
-                foreach (var f in caughtFruit.ToArray())
-                    Explode(f);
-            }
-
-            public void Drop(DrawableHitObject fruit) => removeFromPlateWithTransform(fruit, f =>
-            {
-                f.MoveToY(f.Y + 75, 750, Easing.InSine);
-                f.FadeOut(750);
-            });
-
-            public void Explode(DrawableHitObject fruit)
-            {
-                var originalX = fruit.X * Scale.X;
-
-                removeFromPlateWithTransform(fruit, f =>
-                {
-                    f.MoveToY(f.Y - 50, 250, Easing.OutSine).Then().MoveToY(f.Y + 50, 500, Easing.InSine);
-                    f.MoveToX(f.X + originalX * 6, 1000);
-                    f.FadeOut(750);
-                });
-            }
-
-            private void removeFromPlateWithTransform(DrawableHitObject fruit, Action<DrawableHitObject> action)
-            {
-                if (ExplodingFruitTarget != null)
-                {
-                    fruit.Anchor = Anchor.TopLeft;
-                    fruit.Position = caughtFruit.ToSpaceOfOtherDrawable(fruit.DrawPosition, ExplodingFruitTarget);
-
-                    if (!caughtFruit.Remove(fruit))
-                        // we may have already been removed by a previous operation (due to the weird OnLoadComplete scheduling).
-                        // this avoids a crash on potentially attempting to Add a fruit to ExplodingFruitTarget twice.
-                        return;
-
-                    ExplodingFruitTarget.Add(fruit);
-                }
-
-                double actionTime = Clock.CurrentTime;
-
-                fruit.ApplyCustomUpdateState += onFruitOnApplyCustomUpdateState;
-                onFruitOnApplyCustomUpdateState(fruit, fruit.State.Value);
-
-                void onFruitOnApplyCustomUpdateState(DrawableHitObject o, ArmedState state)
-                {
-                    using (fruit.BeginAbsoluteSequence(actionTime))
-                        action(fruit);
-
-                    fruit.Expire();
-                }
-            }
-        }
-    }
-
-    public class HitExplosion : CompositeDrawable
-    {
-        private readonly CircularContainer largeFaint;
-
-        public HitExplosion(DrawableCatchHitObject fruit)
-        {
-            Size = new Vector2(20);
-            Anchor = Anchor.TopCentre;
-            Origin = Anchor.BottomCentre;
-
-            Color4 objectColour = fruit.AccentColour.Value;
-
-            // scale roughly in-line with visual appearance of notes
-
-            const float angle_variangle = 15; // should be less than 45
-
-            const float roundness = 100;
-
-            const float initial_height = 10;
-
-            var colour = Interpolation.ValueAt(0.4f, objectColour, Color4.White, 0, 1);
-
-            InternalChildren = new Drawable[]
-            {
-                largeFaint = new CircularContainer
-                {
-                    Anchor = Anchor.Centre,
-                    Origin = Anchor.Centre,
-                    RelativeSizeAxes = Axes.Both,
-                    Masking = true,
-                    // we want our size to be very small so the glow dominates it.
-                    Size = new Vector2(0.8f),
-                    Blending = BlendingParameters.Additive,
-                    EdgeEffect = new EdgeEffectParameters
-                    {
-                        Type = EdgeEffectType.Glow,
-                        Colour = Interpolation.ValueAt(0.1f, objectColour, Color4.White, 0, 1).Opacity(0.3f),
-                        Roundness = 160,
-                        Radius = 200,
-                    },
-                },
-                new CircularContainer
-                {
-                    Anchor = Anchor.Centre,
-                    Origin = Anchor.Centre,
-                    RelativeSizeAxes = Axes.Both,
-                    Masking = true,
-                    Blending = BlendingParameters.Additive,
-                    EdgeEffect = new EdgeEffectParameters
-                    {
-                        Type = EdgeEffectType.Glow,
-                        Colour = Interpolation.ValueAt(0.6f, objectColour, Color4.White, 0, 1),
-                        Roundness = 20,
-                        Radius = 50,
-                    },
-                },
-                new CircularContainer
-                {
-                    Anchor = Anchor.Centre,
-                    Origin = Anchor.Centre,
-                    RelativeSizeAxes = Axes.Both,
-                    Masking = true,
-                    Size = new Vector2(0.01f, initial_height),
-                    Blending = BlendingParameters.Additive,
-                    Rotation = RNG.NextSingle(-angle_variangle, angle_variangle),
-                    EdgeEffect = new EdgeEffectParameters
-                    {
-                        Type = EdgeEffectType.Glow,
-                        Colour = colour,
-                        Roundness = roundness,
-                        Radius = 40,
-                    },
-                },
-                new CircularContainer
-                {
-                    Anchor = Anchor.Centre,
-                    Origin = Anchor.Centre,
-                    RelativeSizeAxes = Axes.Both,
-                    Masking = true,
-                    Size = new Vector2(0.01f, initial_height),
-                    Blending = BlendingParameters.Additive,
-                    Rotation = RNG.NextSingle(-angle_variangle, angle_variangle),
-                    EdgeEffect = new EdgeEffectParameters
-                    {
-                        Type = EdgeEffectType.Glow,
-                        Colour = colour,
-                        Roundness = roundness,
-                        Radius = 40,
-                    },
-                }
-            };
-        }
-
-        protected override void LoadComplete()
-        {
-            base.LoadComplete();
-
-            const double duration = 400;
-
-            largeFaint
-                .ResizeTo(largeFaint.Size * new Vector2(5, 1), duration, Easing.OutQuint)
-                .FadeOut(duration * 2);
-
-            this.FadeInFromZero(50).Then().FadeOut(duration, Easing.Out);
-            Expire(true);
-        }
+        protected internal readonly Catcher MovableCatcher;
     }
 }
diff --git a/osu.Game.Rulesets.Catch/UI/HitExplosion.cs b/osu.Game.Rulesets.Catch/UI/HitExplosion.cs
new file mode 100644
index 0000000000..04a86f83be
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/UI/HitExplosion.cs
@@ -0,0 +1,122 @@
+// 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.Extensions.Color4Extensions;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Effects;
+using osu.Framework.Utils;
+using osu.Game.Rulesets.Catch.Objects.Drawables;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Catch.UI
+{
+    public class HitExplosion : CompositeDrawable
+    {
+        private readonly CircularContainer largeFaint;
+
+        public HitExplosion(DrawableCatchHitObject fruit)
+        {
+            Size = new Vector2(20);
+            Anchor = Anchor.TopCentre;
+            Origin = Anchor.BottomCentre;
+
+            Color4 objectColour = fruit.AccentColour.Value;
+
+            // scale roughly in-line with visual appearance of notes
+
+            const float angle_variangle = 15; // should be less than 45
+
+            const float roundness = 100;
+
+            const float initial_height = 10;
+
+            var colour = Interpolation.ValueAt(0.4f, objectColour, Color4.White, 0, 1);
+
+            InternalChildren = new Drawable[]
+            {
+                largeFaint = new CircularContainer
+                {
+                    Anchor = Anchor.Centre,
+                    Origin = Anchor.Centre,
+                    RelativeSizeAxes = Axes.Both,
+                    Masking = true,
+                    // we want our size to be very small so the glow dominates it.
+                    Size = new Vector2(0.8f),
+                    Blending = BlendingParameters.Additive,
+                    EdgeEffect = new EdgeEffectParameters
+                    {
+                        Type = EdgeEffectType.Glow,
+                        Colour = Interpolation.ValueAt(0.1f, objectColour, Color4.White, 0, 1).Opacity(0.3f),
+                        Roundness = 160,
+                        Radius = 200,
+                    },
+                },
+                new CircularContainer
+                {
+                    Anchor = Anchor.Centre,
+                    Origin = Anchor.Centre,
+                    RelativeSizeAxes = Axes.Both,
+                    Masking = true,
+                    Blending = BlendingParameters.Additive,
+                    EdgeEffect = new EdgeEffectParameters
+                    {
+                        Type = EdgeEffectType.Glow,
+                        Colour = Interpolation.ValueAt(0.6f, objectColour, Color4.White, 0, 1),
+                        Roundness = 20,
+                        Radius = 50,
+                    },
+                },
+                new CircularContainer
+                {
+                    Anchor = Anchor.Centre,
+                    Origin = Anchor.Centre,
+                    RelativeSizeAxes = Axes.Both,
+                    Masking = true,
+                    Size = new Vector2(0.01f, initial_height),
+                    Blending = BlendingParameters.Additive,
+                    Rotation = RNG.NextSingle(-angle_variangle, angle_variangle),
+                    EdgeEffect = new EdgeEffectParameters
+                    {
+                        Type = EdgeEffectType.Glow,
+                        Colour = colour,
+                        Roundness = roundness,
+                        Radius = 40,
+                    },
+                },
+                new CircularContainer
+                {
+                    Anchor = Anchor.Centre,
+                    Origin = Anchor.Centre,
+                    RelativeSizeAxes = Axes.Both,
+                    Masking = true,
+                    Size = new Vector2(0.01f, initial_height),
+                    Blending = BlendingParameters.Additive,
+                    Rotation = RNG.NextSingle(-angle_variangle, angle_variangle),
+                    EdgeEffect = new EdgeEffectParameters
+                    {
+                        Type = EdgeEffectType.Glow,
+                        Colour = colour,
+                        Roundness = roundness,
+                        Radius = 40,
+                    },
+                }
+            };
+        }
+
+        protected override void LoadComplete()
+        {
+            base.LoadComplete();
+
+            const double duration = 400;
+
+            largeFaint
+                .ResizeTo(largeFaint.Size * new Vector2(5, 1), duration, Easing.OutQuint)
+                .FadeOut(duration * 2);
+
+            this.FadeInFromZero(50).Then().FadeOut(duration, Easing.Out);
+            Expire(true);
+        }
+    }
+}
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs
index 7403649184..ccc731779d 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs
@@ -185,7 +185,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
             base.ApplySkin(skin, allowFallback);
 
             bool allowBallTint = skin.GetConfig<OsuSkinConfiguration, bool>(OsuSkinConfiguration.AllowSliderBallTint)?.Value ?? false;
-            Ball.Colour = allowBallTint ? AccentColour.Value : Color4.White;
+            Ball.AccentColour = allowBallTint ? AccentColour.Value : Color4.White;
         }
 
         protected override void CheckForResult(bool userTriggered, double timeOffset)
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs
index c871089acd..5a6dd49c44 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs
@@ -16,16 +16,24 @@ using osu.Game.Rulesets.Osu.Skinning;
 using osuTK.Graphics;
 using osu.Game.Skinning;
 using osuTK;
+using osu.Game.Graphics;
 
 namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
 {
-    public class SliderBall : CircularContainer, ISliderProgress, IRequireHighFrequencyMousePosition
+    public class SliderBall : CircularContainer, ISliderProgress, IRequireHighFrequencyMousePosition, IHasAccentColour
     {
         public Func<OsuAction?> GetInitialHitAction;
 
+        public Color4 AccentColour
+        {
+            get => ball.Colour;
+            set => ball.Colour = value;
+        }
+
         private readonly Slider slider;
         private readonly Drawable followCircle;
         private readonly DrawableSlider drawableSlider;
+        private readonly CircularContainer ball;
 
         public SliderBall(Slider slider, DrawableSlider drawableSlider = null)
         {
@@ -47,7 +55,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
                     Alpha = 0,
                     Child = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SliderFollowCircle), _ => new DefaultFollowCircle()),
                 },
-                new CircularContainer
+                ball = new CircularContainer
                 {
                     Masking = true,
                     RelativeSizeAxes = Axes.Both,
diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs b/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs
index c76d4fd5b8..7a257a1603 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs
@@ -12,7 +12,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.Chat;
 using osu.Game.Overlays;
 using osu.Game.Overlays.Chat;
@@ -78,7 +77,7 @@ namespace osu.Game.Tests.Visual.Online
 
             AddAssert($"msg #{index} has {linkAmount} link(s)", () => newLine.Message.Links.Count == linkAmount);
             AddAssert($"msg #{index} has the right action", hasExpectedActions);
-            AddAssert($"msg #{index} is " + (isAction ? "italic" : "not italic"), () => newLine.ContentFlow.Any() && isAction == isItalic());
+            //AddAssert($"msg #{index} is " + (isAction ? "italic" : "not italic"), () => newLine.ContentFlow.Any() && isAction == isItalic());
             AddAssert($"msg #{index} shows {linkAmount} link(s)", isShowingLinks);
 
             bool hasExpectedActions()
@@ -97,7 +96,7 @@ namespace osu.Game.Tests.Visual.Online
                 return true;
             }
 
-            bool isItalic() => newLine.ContentFlow.Where(d => d is OsuSpriteText).Cast<OsuSpriteText>().All(sprite => sprite.Font.Italics);
+            //bool isItalic() => newLine.ContentFlow.Where(d => d is OsuSpriteText).Cast<OsuSpriteText>().All(sprite => sprite.Font.Italics);
 
             bool isShowingLinks()
             {
diff --git a/osu.Game.Tournament/Components/ControlPanel.cs b/osu.Game.Tournament/Components/ControlPanel.cs
index fa5c941f1a..ef8c8767e0 100644
--- a/osu.Game.Tournament/Components/ControlPanel.cs
+++ b/osu.Game.Tournament/Components/ControlPanel.cs
@@ -22,9 +22,9 @@ namespace osu.Game.Tournament.Components
 
         public ControlPanel()
         {
-            RelativeSizeAxes = Axes.Both;
+            RelativeSizeAxes = Axes.Y;
             AlwaysPresent = true;
-            Width = 0.15f;
+            Width = TournamentSceneManager.CONTROL_AREA_WIDTH;
             Anchor = Anchor.TopRight;
 
             InternalChildren = new Drawable[]
@@ -47,8 +47,8 @@ namespace osu.Game.Tournament.Components
                     Origin = Anchor.TopCentre,
                     RelativeSizeAxes = Axes.X,
                     AutoSizeAxes = Axes.Y,
-                    Width = 0.75f,
                     Position = new Vector2(0, 35f),
+                    Padding = new MarginPadding(5),
                     Direction = FillDirection.Vertical,
                     Spacing = new Vector2(0, 5f),
                 },
diff --git a/osu.Game.Tournament/Screens/SetupScreen.cs b/osu.Game.Tournament/Screens/SetupScreen.cs
index 023582166c..b7f8b2bfd6 100644
--- a/osu.Game.Tournament/Screens/SetupScreen.cs
+++ b/osu.Game.Tournament/Screens/SetupScreen.cs
@@ -3,7 +3,10 @@
 
 using System;
 using System.Collections.Generic;
+using System.Drawing;
 using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Configuration;
 using osu.Framework.Graphics;
 using osu.Framework.Graphics.Containers;
 using osu.Game.Graphics.UserInterface;
@@ -22,6 +25,7 @@ namespace osu.Game.Tournament.Screens
         private FillFlowContainer fillFlow;
 
         private LoginOverlay loginOverlay;
+        private ActionableInfo resolution;
 
         [Resolved]
         private MatchIPCInfo ipc { get; set; }
@@ -32,9 +36,13 @@ namespace osu.Game.Tournament.Screens
         [Resolved]
         private RulesetStore rulesets { get; set; }
 
+        private Bindable<Size> windowSize;
+
         [BackgroundDependencyLoader]
-        private void load()
+        private void load(FrameworkConfigManager frameworkConfig)
         {
+            windowSize = frameworkConfig.GetBindable<Size>(FrameworkSetting.WindowedSize);
+
             InternalChild = fillFlow = new FillFlowContainer
             {
                 RelativeSizeAxes = Axes.X,
@@ -48,6 +56,9 @@ namespace osu.Game.Tournament.Screens
             reload();
         }
 
+        [Resolved]
+        private Framework.Game game { get; set; }
+
         private void reload()
         {
             var fileBasedIpc = ipc as FileBasedIPC;
@@ -97,9 +108,25 @@ namespace osu.Game.Tournament.Screens
                     Items = rulesets.AvailableRulesets,
                     Current = LadderInfo.Ruleset,
                 },
+                resolution = new ActionableInfo
+                {
+                    Label = "Stream area resolution",
+                    ButtonText = "Set to 1080p",
+                    Action = () =>
+                    {
+                        windowSize.Value = new Size((int)(1920 / TournamentSceneManager.STREAM_AREA_WIDTH * TournamentSceneManager.REQUIRED_WIDTH), 1080);
+                    }
+                }
             };
         }
 
+        protected override void Update()
+        {
+            base.Update();
+
+            resolution.Value = $"{ScreenSpaceDrawQuad.Width:N0}x{ScreenSpaceDrawQuad.Height:N0}";
+        }
+
         public class LabelledDropdown<T> : LabelledComponent<OsuDropdown<T>, T>
         {
             public LabelledDropdown()
diff --git a/osu.Game.Tournament/TournamentGameBase.cs b/osu.Game.Tournament/TournamentGameBase.cs
index 41822ae2c3..85db9e61fb 100644
--- a/osu.Game.Tournament/TournamentGameBase.cs
+++ b/osu.Game.Tournament/TournamentGameBase.cs
@@ -65,7 +65,7 @@ namespace osu.Game.Tournament
             windowSize = frameworkConfig.GetBindable<Size>(FrameworkSetting.WindowedSize);
             windowSize.BindValueChanged(size => ScheduleAfterChildren(() =>
             {
-                var minWidth = (int)(size.NewValue.Height / 9f * 16 + 400);
+                var minWidth = (int)(size.NewValue.Height / 768f * TournamentSceneManager.REQUIRED_WIDTH) - 1;
 
                 heightWarning.Alpha = size.NewValue.Width < minWidth ? 1 : 0;
             }), true);
diff --git a/osu.Game.Tournament/TournamentSceneManager.cs b/osu.Game.Tournament/TournamentSceneManager.cs
index ef8d16011d..23fcb01db7 100644
--- a/osu.Game.Tournament/TournamentSceneManager.cs
+++ b/osu.Game.Tournament/TournamentSceneManager.cs
@@ -33,6 +33,12 @@ namespace osu.Game.Tournament
         private Container screens;
         private TourneyVideo video;
 
+        public const float CONTROL_AREA_WIDTH = 160;
+
+        public const float STREAM_AREA_WIDTH = 1366;
+
+        public const double REQUIRED_WIDTH = TournamentSceneManager.CONTROL_AREA_WIDTH * 2 + TournamentSceneManager.STREAM_AREA_WIDTH;
+
         [Cached]
         private TournamentMatchChatDisplay chat = new TournamentMatchChatDisplay();
 
@@ -51,13 +57,13 @@ namespace osu.Game.Tournament
             {
                 new Container
                 {
-                    RelativeSizeAxes = Axes.Both,
-                    X = 200,
+                    RelativeSizeAxes = Axes.Y,
+                    X = CONTROL_AREA_WIDTH,
                     FillMode = FillMode.Fit,
                     FillAspectRatio = 16 / 9f,
                     Anchor = Anchor.TopLeft,
                     Origin = Anchor.TopLeft,
-                    Size = new Vector2(0.8f, 1),
+                    Width = STREAM_AREA_WIDTH,
                     //Masking = true,
                     Children = new Drawable[]
                     {
@@ -96,7 +102,7 @@ namespace osu.Game.Tournament
                 new Container
                 {
                     RelativeSizeAxes = Axes.Y,
-                    Width = 200,
+                    Width = CONTROL_AREA_WIDTH,
                     Children = new Drawable[]
                     {
                         new Box
@@ -108,8 +114,8 @@ namespace osu.Game.Tournament
                         {
                             RelativeSizeAxes = Axes.Both,
                             Direction = FillDirection.Vertical,
-                            Spacing = new Vector2(2),
-                            Padding = new MarginPadding(2),
+                            Spacing = new Vector2(5),
+                            Padding = new MarginPadding(5),
                             Children = new Drawable[]
                             {
                                 new ScreenButton(typeof(SetupScreen)) { Text = "Setup", RequestSelection = SetScreen },
diff --git a/osu.Game/Graphics/OsuFont.cs b/osu.Game/Graphics/OsuFont.cs
index 841936d2c5..7c78141b4d 100644
--- a/osu.Game/Graphics/OsuFont.cs
+++ b/osu.Game/Graphics/OsuFont.cs
@@ -30,8 +30,15 @@ namespace osu.Game.Graphics
         /// <param name="italics">Whether the font is italic.</param>
         /// <param name="fixedWidth">Whether all characters should be spaced the same distance apart.</param>
         /// <returns>The <see cref="FontUsage"/>.</returns>
-        public static FontUsage GetFont(Typeface typeface = Typeface.Exo, float size = DEFAULT_FONT_SIZE, FontWeight weight = FontWeight.Medium, bool italics = false, bool fixedWidth = false)
-            => new FontUsage(GetFamilyString(typeface), size, GetWeightString(typeface, weight), italics, fixedWidth);
+        public static FontUsage GetFont(Typeface typeface = Typeface.Torus, float size = DEFAULT_FONT_SIZE, FontWeight weight = FontWeight.Medium, bool italics = false, bool fixedWidth = false)
+            => new FontUsage(GetFamilyString(typeface), size, GetWeightString(typeface, weight), getItalics(italics), fixedWidth);
+
+        private static bool getItalics(in bool italicsRequested)
+        {
+            // right now none of our fonts support italics.
+            // should add exceptions to this rule if they come up.
+            return false;
+        }
 
         /// <summary>
         /// Retrieves the string representation of a <see cref="Typeface"/>.
@@ -42,9 +49,6 @@ namespace osu.Game.Graphics
         {
             switch (typeface)
             {
-                case Typeface.Exo:
-                    return "Exo2.0";
-
                 case Typeface.Venera:
                     return "Venera";
 
@@ -62,7 +66,13 @@ namespace osu.Game.Graphics
         /// <param name="weight">The <see cref="FontWeight"/>.</param>
         /// <returns>The string representation of <paramref name="weight"/> in the specified <paramref name="typeface"/>.</returns>
         public static string GetWeightString(Typeface typeface, FontWeight weight)
-            => GetWeightString(GetFamilyString(typeface), weight);
+        {
+            if (typeface == Typeface.Torus && weight == FontWeight.Medium)
+                // torus doesn't have a medium; fallback to regular.
+                weight = FontWeight.Regular;
+
+            return GetWeightString(GetFamilyString(typeface), weight);
+        }
 
         /// <summary>
         /// Retrieves the string representation of a <see cref="FontWeight"/>.
@@ -96,7 +106,6 @@ namespace osu.Game.Graphics
 
     public enum Typeface
     {
-        Exo,
         Venera,
         Torus
     }
diff --git a/osu.Game/Graphics/UserInterface/OsuTabControl.cs b/osu.Game/Graphics/UserInterface/OsuTabControl.cs
index 6c883d9893..ca9f1330f9 100644
--- a/osu.Game/Graphics/UserInterface/OsuTabControl.cs
+++ b/osu.Game/Graphics/UserInterface/OsuTabControl.cs
@@ -173,7 +173,7 @@ namespace osu.Game.Graphics.UserInterface
                     new HoverClickSounds()
                 };
 
-                Active.BindValueChanged(active => Text.Font = Text.Font.With(Typeface.Exo, weight: active.NewValue ? FontWeight.Bold : FontWeight.Medium), true);
+                Active.BindValueChanged(active => Text.Font = Text.Font.With(Typeface.Torus, weight: active.NewValue ? FontWeight.Bold : FontWeight.Medium), true);
             }
 
             protected override void OnActivated() => fadeActive();
diff --git a/osu.Game/Graphics/UserInterface/PageTabControl.cs b/osu.Game/Graphics/UserInterface/PageTabControl.cs
index ddcb626701..d05a08108a 100644
--- a/osu.Game/Graphics/UserInterface/PageTabControl.cs
+++ b/osu.Game/Graphics/UserInterface/PageTabControl.cs
@@ -78,7 +78,7 @@ namespace osu.Game.Graphics.UserInterface
                     new HoverClickSounds()
                 };
 
-                Active.BindValueChanged(active => Text.Font = Text.Font.With(Typeface.Exo, weight: active.NewValue ? FontWeight.Bold : FontWeight.Medium), true);
+                Active.BindValueChanged(active => Text.Font = Text.Font.With(Typeface.Torus, weight: active.NewValue ? FontWeight.Bold : FontWeight.Medium), true);
             }
 
             protected virtual string CreateText() => (Value as Enum)?.GetDescription() ?? Value.ToString();
diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs
index b2277e2abf..3c7ab27651 100644
--- a/osu.Game/OsuGameBase.cs
+++ b/osu.Game/OsuGameBase.cs
@@ -138,30 +138,17 @@ namespace osu.Game
             dependencies.Cache(LocalConfig);
 
             AddFont(Resources, @"Fonts/osuFont");
-            AddFont(Resources, @"Fonts/Exo2.0-Medium");
-            AddFont(Resources, @"Fonts/Exo2.0-MediumItalic");
+
+            AddFont(Resources, @"Fonts/Torus-Regular");
+            AddFont(Resources, @"Fonts/Torus-Light");
+            AddFont(Resources, @"Fonts/Torus-SemiBold");
+            AddFont(Resources, @"Fonts/Torus-Bold");
 
             AddFont(Resources, @"Fonts/Noto-Basic");
             AddFont(Resources, @"Fonts/Noto-Hangul");
             AddFont(Resources, @"Fonts/Noto-CJK-Basic");
             AddFont(Resources, @"Fonts/Noto-CJK-Compatibility");
 
-            AddFont(Resources, @"Fonts/Exo2.0-Regular");
-            AddFont(Resources, @"Fonts/Exo2.0-RegularItalic");
-            AddFont(Resources, @"Fonts/Exo2.0-SemiBold");
-            AddFont(Resources, @"Fonts/Exo2.0-SemiBoldItalic");
-            AddFont(Resources, @"Fonts/Exo2.0-Bold");
-            AddFont(Resources, @"Fonts/Exo2.0-BoldItalic");
-            AddFont(Resources, @"Fonts/Exo2.0-Light");
-            AddFont(Resources, @"Fonts/Exo2.0-LightItalic");
-            AddFont(Resources, @"Fonts/Exo2.0-Black");
-            AddFont(Resources, @"Fonts/Exo2.0-BlackItalic");
-
-            AddFont(Resources, @"Fonts/Torus-SemiBold");
-            AddFont(Resources, @"Fonts/Torus-Bold");
-            AddFont(Resources, @"Fonts/Torus-Regular");
-            AddFont(Resources, @"Fonts/Torus-Light");
-
             AddFont(Resources, @"Fonts/Venera-Light");
             AddFont(Resources, @"Fonts/Venera-Bold");
             AddFont(Resources, @"Fonts/Venera-Black");
diff --git a/osu.Game/Overlays/AccountCreation/ScreenEntry.cs b/osu.Game/Overlays/AccountCreation/ScreenEntry.cs
index 2576900db8..a0b1b27ebf 100644
--- a/osu.Game/Overlays/AccountCreation/ScreenEntry.cs
+++ b/osu.Game/Overlays/AccountCreation/ScreenEntry.cs
@@ -132,7 +132,7 @@ namespace osu.Game.Overlays.AccountCreation
             usernameDescription.AddText("This will be your public presence. No profanity, no impersonation. Avoid exposing your own personal details, too!");
 
             emailAddressDescription.AddText("Will be used for notifications, account verification and in the case you forget your password. No spam, ever.");
-            emailAddressDescription.AddText(" Make sure to get it right!", cp => cp.Font = cp.Font.With(Typeface.Exo, weight: FontWeight.Bold));
+            emailAddressDescription.AddText(" Make sure to get it right!", cp => cp.Font = cp.Font.With(Typeface.Torus, weight: FontWeight.Bold));
 
             passwordDescription.AddText("At least ");
             characterCheckText = passwordDescription.AddText("8 characters long");
diff --git a/osu.Game/Overlays/Changelog/ChangelogBuild.cs b/osu.Game/Overlays/Changelog/ChangelogBuild.cs
index 8aee76cb08..48bf6c2ddd 100644
--- a/osu.Game/Overlays/Changelog/ChangelogBuild.cs
+++ b/osu.Game/Overlays/Changelog/ChangelogBuild.cs
@@ -93,6 +93,7 @@ namespace osu.Game.Overlays.Changelog
                                 Direction = FillDirection.Full,
                                 RelativeSizeAxes = Axes.X,
                                 AutoSizeAxes = Axes.Y,
+                                TextAnchor = Anchor.BottomLeft,
                             }
                         }
                     };
@@ -125,7 +126,7 @@ namespace osu.Game.Overlays.Changelog
 
                     title.AddText("by ", t =>
                     {
-                        t.Font = fontMedium.With(italics: true);
+                        t.Font = fontMedium;
                         t.Colour = entryColour;
                         t.Padding = new MarginPadding { Left = 10 };
                     });
@@ -138,7 +139,7 @@ namespace osu.Game.Overlays.Changelog
                             Id = entry.GithubUser.UserId.Value
                         }, t =>
                         {
-                            t.Font = fontMedium.With(italics: true);
+                            t.Font = fontMedium;
                             t.Colour = entryColour;
                         });
                     }
@@ -146,7 +147,7 @@ namespace osu.Game.Overlays.Changelog
                     {
                         title.AddLink(entry.GithubUser.DisplayName, entry.GithubUser.GithubUrl, t =>
                         {
-                            t.Font = fontMedium.With(italics: true);
+                            t.Font = fontMedium;
                             t.Colour = entryColour;
                         });
                     }
@@ -154,7 +155,7 @@ namespace osu.Game.Overlays.Changelog
                     {
                         title.AddText(entry.GithubUser.DisplayName, t =>
                         {
-                            t.Font = fontMedium.With(italics: true);
+                            t.Font = fontMedium;
                             t.Colour = entryColour;
                         });
                     }
diff --git a/osu.Game/Overlays/News/NewsArticleCover.cs b/osu.Game/Overlays/News/NewsArticleCover.cs
index f61b30b381..e381b629e4 100644
--- a/osu.Game/Overlays/News/NewsArticleCover.cs
+++ b/osu.Game/Overlays/News/NewsArticleCover.cs
@@ -75,7 +75,7 @@ namespace osu.Game.Overlays.News
                         Left = 25,
                         Bottom = 50,
                     },
-                    Font = OsuFont.GetFont(Typeface.Exo, 24, FontWeight.Bold),
+                    Font = OsuFont.GetFont(Typeface.Torus, 24, FontWeight.Bold),
                     Text = info.Title,
                 },
                 new OsuSpriteText
@@ -87,7 +87,7 @@ namespace osu.Game.Overlays.News
                         Left = 25,
                         Bottom = 30,
                     },
-                    Font = OsuFont.GetFont(Typeface.Exo, 16, FontWeight.Bold),
+                    Font = OsuFont.GetFont(Typeface.Torus, 16, FontWeight.Bold),
                     Text = "by " + info.Author
                 }
             };
@@ -148,7 +148,7 @@ namespace osu.Game.Overlays.News
                     {
                         Anchor = Anchor.Centre,
                         Origin = Anchor.Centre,
-                        Font = OsuFont.GetFont(Typeface.Exo, 12, FontWeight.Black, false, false),
+                        Font = OsuFont.GetFont(Typeface.Torus, 12, FontWeight.Black, false, false),
                         Text = date.ToString("d MMM yyy").ToUpper(),
                         Margin = new MarginPadding
                         {
diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs
index 1fc51d2ce8..8d3ad5984f 100644
--- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs
+++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs
@@ -126,7 +126,7 @@ namespace osu.Game.Rulesets.Objects.Legacy
 
                 if (split.Length > 7)
                 {
-                    length = Math.Max(0, Parsing.ParseDouble(split[7]));
+                    length = Math.Max(0, Parsing.ParseDouble(split[7], Parsing.MAX_COORDINATE_VALUE));
                     if (length == 0)
                         length = null;
                 }
diff --git a/osu.Game/Screens/Menu/Disclaimer.cs b/osu.Game/Screens/Menu/Disclaimer.cs
index ee8200321b..35091028ae 100644
--- a/osu.Game/Screens/Menu/Disclaimer.cs
+++ b/osu.Game/Screens/Menu/Disclaimer.cs
@@ -90,14 +90,14 @@ namespace osu.Game.Screens.Menu
                 }
             };
 
-            textFlow.AddText("This project is an ongoing ", t => t.Font = t.Font.With(Typeface.Exo, 30, FontWeight.Light));
-            textFlow.AddText("work in progress", t => t.Font = t.Font.With(Typeface.Exo, 30, FontWeight.SemiBold));
+            textFlow.AddText("This project is an ongoing ", t => t.Font = t.Font.With(Typeface.Torus, 30, FontWeight.Light));
+            textFlow.AddText("work in progress", t => t.Font = t.Font.With(Typeface.Torus, 30, FontWeight.SemiBold));
 
             textFlow.NewParagraph();
 
             static void format(SpriteText t) => t.Font = OsuFont.GetFont(size: 15, weight: FontWeight.SemiBold);
 
-            textFlow.AddParagraph(getRandomTip(), t => t.Font = t.Font.With(Typeface.Exo, 20, FontWeight.SemiBold));
+            textFlow.AddParagraph(getRandomTip(), t => t.Font = t.Font.With(Typeface.Torus, 20, FontWeight.SemiBold));
             textFlow.NewParagraph();
 
             textFlow.NewParagraph();
diff --git a/osu.Game/Screens/Multi/Lounge/Components/ParticipantInfo.cs b/osu.Game/Screens/Multi/Lounge/Components/ParticipantInfo.cs
index a55db096af..4152a9a3b2 100644
--- a/osu.Game/Screens/Multi/Lounge/Components/ParticipantInfo.cs
+++ b/osu.Game/Screens/Multi/Lounge/Components/ParticipantInfo.cs
@@ -77,7 +77,7 @@ namespace osu.Game.Screens.Multi.Lounge.Components
                 if (host.NewValue != null)
                 {
                     hostText.AddText("hosted by ");
-                    hostText.AddUserLink(host.NewValue, s => s.Font = s.Font.With(Typeface.Exo, weight: FontWeight.Bold, italics: true));
+                    hostText.AddUserLink(host.NewValue, s => s.Font = s.Font.With(Typeface.Torus, weight: FontWeight.Bold, italics: true));
 
                     flagContainer.Child = new UpdateableFlag(host.NewValue.Country) { RelativeSizeAxes = Axes.Both };
                 }
diff --git a/osu.Game/Screens/Multi/Ranking/Pages/RoomLeaderboardPage.cs b/osu.Game/Screens/Multi/Ranking/Pages/RoomLeaderboardPage.cs
index f8fb192b5c..0d31805774 100644
--- a/osu.Game/Screens/Multi/Ranking/Pages/RoomLeaderboardPage.cs
+++ b/osu.Game/Screens/Multi/Ranking/Pages/RoomLeaderboardPage.cs
@@ -91,7 +91,7 @@ namespace osu.Game.Screens.Multi.Ranking.Pages
 
             rankText.AddText($"#{index + 1} ", s =>
             {
-                s.Font = s.Font.With(Typeface.Exo, weight: FontWeight.Bold);
+                s.Font = s.Font.With(Typeface.Torus, weight: FontWeight.Bold);
                 s.Colour = colours.YellowDark;
             });
 
diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj
index ba6f0e2251..95d09e84eb 100644
--- a/osu.Game/osu.Game.csproj
+++ b/osu.Game/osu.Game.csproj
@@ -22,7 +22,7 @@
     <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
     <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
     <PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
-    <PackageReference Include="ppy.osu.Game.Resources" Version="2020.304.0" />
+    <PackageReference Include="ppy.osu.Game.Resources" Version="2020.315.0" />
     <PackageReference Include="ppy.osu.Framework" Version="2020.312.0" />
     <PackageReference Include="Sentry" Version="2.1.0" />
     <PackageReference Include="SharpCompress" Version="0.24.0" />
diff --git a/osu.iOS.props b/osu.iOS.props
index 54cd400d51..ce7ff38988 100644
--- a/osu.iOS.props
+++ b/osu.iOS.props
@@ -70,7 +70,7 @@
     <Reference Include="System.Net.Http" />
   </ItemGroup>
   <ItemGroup Label="Package References">
-    <PackageReference Include="ppy.osu.Game.Resources" Version="2020.304.0" />
+    <PackageReference Include="ppy.osu.Game.Resources" Version="2020.315.0" />
     <PackageReference Include="ppy.osu.Framework.iOS" Version="2020.312.0" />
   </ItemGroup>
   <!-- Xamarin.iOS does not automatically handle transitive dependencies from NuGet packages. -->