From da976000763e6bcf7975714fa971a08b48777bed Mon Sep 17 00:00:00 2001
From: smoogipoo <smoogipoo@smgi.me>
Date: Mon, 8 Oct 2018 16:46:58 +0900
Subject: [PATCH 01/17] Fix inaccurate section lengths for first hitobject

---
 osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs
index 5e91ed7a97..028e3acc57 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs
@@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
             double sectionLength = section_length * timeRate;
 
             // The first object doesn't generate a strain, so we begin with an incremented section end
-            double currentSectionEnd = 2 * sectionLength;
+            double currentSectionEnd = Math.Ceiling(beatmap.HitObjects.First().StartTime / sectionLength) * sectionLength;
 
             foreach (OsuDifficultyHitObject h in difficultyBeatmap)
             {

From 61e7ada977c3567463af8d48a38f7145c2605046 Mon Sep 17 00:00:00 2001
From: smoogipoo <smoogipoo@smgi.me>
Date: Mon, 8 Oct 2018 17:36:06 +0900
Subject: [PATCH 02/17] Use ints + fix position calculation

---
 .../Difficulty/Preprocessing/OsuDifficultyHitObject.cs          | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs
index 29de23406b..7f293fd099 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs
@@ -88,7 +88,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
             var computeVertex = new Action<double>(t =>
             {
                 // ReSharper disable once PossibleInvalidOperationException (bugged in current r# version)
-                var diff = slider.StackedPositionAt(t) - slider.LazyEndPosition.Value;
+                var diff = slider.StackedPositionAt(((int)t - (int)slider.StartTime) / (float)(int)slider.Duration) - slider.LazyEndPosition.Value;
                 float dist = diff.Length;
 
                 if (dist > approxFollowCircleRadius)

From 1ad5090ad68b52da6a49114c5f5187e200bc05cc Mon Sep 17 00:00:00 2001
From: smoogipoo <smoogipoo@smgi.me>
Date: Mon, 8 Oct 2018 17:37:33 +0900
Subject: [PATCH 03/17] Separate travel distance from jump distance

---
 .../Preprocessing/OsuDifficultyHitObject.cs      | 16 +++++++++++-----
 osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs   |  2 +-
 osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs |  2 +-
 3 files changed, 13 insertions(+), 7 deletions(-)

diff --git a/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs
index 7f293fd099..2419709e41 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs
@@ -21,9 +21,14 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
         public OsuHitObject BaseObject { get; }
 
         /// <summary>
-        /// Normalized distance from the <see cref="OsuHitObject.StackedPosition"/> of the previous <see cref="OsuDifficultyHitObject"/>.
+        /// Normalized distance from the end position of the previous <see cref="OsuDifficultyHitObject"/>.
         /// </summary>
-        public double Distance { get; private set; }
+        public double JumpDistance { get; private set; }
+
+        /// <summary>
+        /// Normalized distance from the start position to the end position of the previous <see cref="OsuDifficultyHitObject"/>.
+        /// </summary>
+        public double TravelDistance { get; private set; }
 
         /// <summary>
         /// Milliseconds elapsed since the StartTime of the previous <see cref="OsuDifficultyHitObject"/>.
@@ -51,10 +56,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
         private void setDistances()
         {
             // We will scale distances by this factor, so we can assume a uniform CircleSize among beatmaps.
-            double scalingFactor = normalized_radius / BaseObject.Radius;
+            float scalingFactor = normalized_radius / (float)BaseObject.Radius;
             if (BaseObject.Radius < 30)
             {
-                double smallCircleBonus = Math.Min(30 - BaseObject.Radius, 5) / 50;
+                float smallCircleBonus = Math.Min(30 - (float)BaseObject.Radius, 5) / 50;
                 scalingFactor *= 1 + smallCircleBonus;
             }
 
@@ -69,7 +74,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
                 lastTravelDistance = lastSlider.LazyTravelDistance;
             }
 
-            Distance = (lastTravelDistance + (BaseObject.StackedPosition - lastCursorPosition).Length) * scalingFactor;
+            JumpDistance = (BaseObject.StackedPosition - lastCursorPosition).Length * scalingFactor;
+            TravelDistance = lastTravelDistance * scalingFactor;
         }
 
         private void setTimingValues()
diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs
index 0a45c62671..9ffdd280aa 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs
@@ -14,6 +14,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
         protected override double SkillMultiplier => 26.25;
         protected override double StrainDecayBase => 0.15;
 
-        protected override double StrainValueOf(OsuDifficultyHitObject current) => Math.Pow(current.Distance, 0.99) / current.DeltaTime;
+        protected override double StrainValueOf(OsuDifficultyHitObject current) => (Math.Pow(current.TravelDistance, 0.99) + Math.Pow(current.JumpDistance, 0.99)) / current.DeltaTime;
     }
 }
diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs
index b807f20037..501a8e8e01 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs
@@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
 
         protected override double StrainValueOf(OsuDifficultyHitObject current)
         {
-            double distance = current.Distance;
+            double distance = current.TravelDistance + current.JumpDistance;
 
             double speedValue;
             if (distance > single_spacing_threshold)

From 35f45e74dc2e5e41da3dd186e78652b329610687 Mon Sep 17 00:00:00 2001
From: smoogipoo <smoogipoo@smgi.me>
Date: Mon, 8 Oct 2018 17:39:10 +0900
Subject: [PATCH 04/17] Calculate scaled positions prior to square-rooting

---
 .../Difficulty/Preprocessing/OsuDifficultyHitObject.cs          | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs
index 2419709e41..b9eaf9ff04 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs
@@ -74,7 +74,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
                 lastTravelDistance = lastSlider.LazyTravelDistance;
             }
 
-            JumpDistance = (BaseObject.StackedPosition - lastCursorPosition).Length * scalingFactor;
+            JumpDistance = (BaseObject.StackedPosition * scalingFactor - lastCursorPosition * scalingFactor).Length;
             TravelDistance = lastTravelDistance * scalingFactor;
         }
 

From 0116db95d0363f0ac15932980a307fd7ff458b5f Mon Sep 17 00:00:00 2001
From: smoogipoo <smoogipoo@smgi.me>
Date: Mon, 8 Oct 2018 18:37:30 +0900
Subject: [PATCH 05/17] Fix progress calculation

---
 .../Difficulty/Preprocessing/OsuDifficultyHitObject.cs   | 9 ++++++++-
 1 file changed, 8 insertions(+), 1 deletion(-)

diff --git a/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs
index b9eaf9ff04..26db2e3faa 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs
@@ -3,6 +3,7 @@
 
 using System;
 using System.Linq;
+using osu.Game.Rulesets.Objects.Types;
 using osu.Game.Rulesets.Osu.Objects;
 using OpenTK;
 
@@ -93,8 +94,14 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
             float approxFollowCircleRadius = (float)(slider.Radius * 3);
             var computeVertex = new Action<double>(t =>
             {
+                double progress = ((int)t - (int)slider.StartTime) / (float)(int)slider.SpanDuration;
+                if (progress % 2 > 1)
+                    progress = 1 - progress % 1;
+                else
+                    progress = progress % 1;
+
                 // ReSharper disable once PossibleInvalidOperationException (bugged in current r# version)
-                var diff = slider.StackedPositionAt(((int)t - (int)slider.StartTime) / (float)(int)slider.Duration) - slider.LazyEndPosition.Value;
+                var diff = slider.StackedPosition + slider.Curve.PositionAt(progress) - slider.LazyEndPosition.Value;
                 float dist = diff.Length;
 
                 if (dist > approxFollowCircleRadius)

From f8eaccddda1d1b1657127e6ae29778d909904edb Mon Sep 17 00:00:00 2001
From: smoogipoo <smoogipoo@smgi.me>
Date: Mon, 8 Oct 2018 18:37:49 +0900
Subject: [PATCH 06/17] Stable doesn't use stacked positions

---
 .../Difficulty/Preprocessing/OsuDifficultyHitObject.cs    | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs
index 26db2e3faa..44bec47e14 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs
@@ -64,7 +64,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
                 scalingFactor *= 1 + smallCircleBonus;
             }
 
-            Vector2 lastCursorPosition = lastObject.StackedPosition;
+            Vector2 lastCursorPosition = lastObject.Position;
             float lastTravelDistance = 0;
 
             var lastSlider = lastObject as Slider;
@@ -75,7 +75,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
                 lastTravelDistance = lastSlider.LazyTravelDistance;
             }
 
-            JumpDistance = (BaseObject.StackedPosition * scalingFactor - lastCursorPosition * scalingFactor).Length;
+            JumpDistance = (BaseObject.Position * scalingFactor - lastCursorPosition * scalingFactor).Length;
             TravelDistance = lastTravelDistance * scalingFactor;
         }
 
@@ -89,7 +89,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
         {
             if (slider.LazyEndPosition != null)
                 return;
-            slider.LazyEndPosition = slider.StackedPosition;
+            slider.LazyEndPosition = slider.Position;
 
             float approxFollowCircleRadius = (float)(slider.Radius * 3);
             var computeVertex = new Action<double>(t =>
@@ -101,7 +101,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
                     progress = progress % 1;
 
                 // ReSharper disable once PossibleInvalidOperationException (bugged in current r# version)
-                var diff = slider.StackedPosition + slider.Curve.PositionAt(progress) - slider.LazyEndPosition.Value;
+                var diff = slider.Position + slider.Curve.PositionAt(progress) - slider.LazyEndPosition.Value;
                 float dist = diff.Length;
 
                 if (dist > approxFollowCircleRadius)

From b7499fa95627d054e9a3a881420a92808cf12ebe Mon Sep 17 00:00:00 2001
From: smoogipoo <smoogipoo@smgi.me>
Date: Tue, 9 Oct 2018 11:34:38 +0900
Subject: [PATCH 07/17] Allow TimingControlPoint to be overridden

---
 .../Beatmaps/ControlPoints/TimingControlPoint.cs   |  2 +-
 osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs  | 14 ++++++++------
 2 files changed, 9 insertions(+), 7 deletions(-)

diff --git a/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs
index eb60133fed..81eddaa43a 100644
--- a/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs
+++ b/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs
@@ -16,7 +16,7 @@ namespace osu.Game.Beatmaps.ControlPoints
         /// <summary>
         /// The beat length at this control point.
         /// </summary>
-        public double BeatLength
+        public virtual double BeatLength
         {
             get => beatLength;
             set => beatLength = MathHelper.Clamp(value, 6, 60000);
diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs
index 181d17932d..5b5bc5d936 100644
--- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs
+++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs
@@ -318,12 +318,12 @@ namespace osu.Game.Beatmaps.Formats
 
                 if (timingChange)
                 {
-                    handleTimingControlPoint(new TimingControlPoint
-                    {
-                        Time = time,
-                        BeatLength = beatLength,
-                        TimeSignature = timeSignature
-                    });
+                    var controlPoint = CreateTimingControlPoint();
+                    controlPoint.Time = time;
+                    controlPoint.BeatLength = beatLength;
+                    controlPoint.TimeSignature = timeSignature;
+
+                    handleTimingControlPoint(controlPoint);
                 }
 
                 handleDifficultyControlPoint(new DifficultyControlPoint
@@ -418,6 +418,8 @@ namespace osu.Game.Beatmaps.Formats
 
         private double getOffsetTime(double time) => time + (ApplyOffsets ? offset : 0);
 
+        protected virtual TimingControlPoint CreateTimingControlPoint() => new TimingControlPoint();
+
         [Flags]
         internal enum EffectFlags
         {

From 9facf707be57018b816417d6a8677f5719d87c35 Mon Sep 17 00:00:00 2001
From: smoogipoo <smoogipoo@smgi.me>
Date: Tue, 9 Oct 2018 11:49:24 +0900
Subject: [PATCH 08/17] Add diffcalc beatmap decoder

---
 .../Preprocessing/OsuDifficultyHitObject.cs   |  1 -
 ...egacyDifficultyCalculatorBeatmapDecoder.cs | 36 +++++++++++++++++++
 2 files changed, 36 insertions(+), 1 deletion(-)
 create mode 100644 osu.Game/Beatmaps/Formats/LegacyDifficultyCalculatorBeatmapDecoder.cs

diff --git a/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs
index 44bec47e14..448dc140a3 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs
@@ -3,7 +3,6 @@
 
 using System;
 using System.Linq;
-using osu.Game.Rulesets.Objects.Types;
 using osu.Game.Rulesets.Osu.Objects;
 using OpenTK;
 
diff --git a/osu.Game/Beatmaps/Formats/LegacyDifficultyCalculatorBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyDifficultyCalculatorBeatmapDecoder.cs
new file mode 100644
index 0000000000..61efd329c0
--- /dev/null
+++ b/osu.Game/Beatmaps/Formats/LegacyDifficultyCalculatorBeatmapDecoder.cs
@@ -0,0 +1,36 @@
+// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
+// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
+
+using System.Linq;
+using osu.Game.Beatmaps.ControlPoints;
+
+namespace osu.Game.Beatmaps.Formats
+{
+    /// <summary>
+    /// A <see cref="LegacyBeatmapDecoder"/> built for difficulty calculation of legacy <see cref="Beatmap"/>s
+    /// <remarks>
+    /// To use this, the decoder must be registered by the application through <see cref="LegacyDifficultyCalculatorBeatmapDecoder.Register"/>.
+    /// Doing so will override any existing <see cref="Beatmap"/> decoders.
+    /// </remarks>
+    /// </summary>
+    public class LegacyDifficultyCalculatorBeatmapDecoder : LegacyBeatmapDecoder
+    {
+        public LegacyDifficultyCalculatorBeatmapDecoder(int version = LATEST_VERSION)
+            : base(version)
+        {
+        }
+
+        public new static void Register()
+        {
+            AddDecoder<Beatmap>(@"osu file format v", m => new LegacyDifficultyCalculatorBeatmapDecoder(int.Parse(m.Split('v').Last())));
+        }
+
+        protected override TimingControlPoint CreateTimingControlPoint()
+            => new LegacyDifficultyCalculatorControlPoint();
+
+        private class LegacyDifficultyCalculatorControlPoint : TimingControlPoint
+        {
+            public override double BeatLength { get; set; } = 1000;
+        }
+    }
+}

From 0a3be0d253c076bfda1a95ebb26854808be30293 Mon Sep 17 00:00:00 2001
From: smoogipoo <smoogipoo@smgi.me>
Date: Tue, 9 Oct 2018 12:03:47 +0900
Subject: [PATCH 09/17] Adjust comments slightly

---
 .../Difficulty/Preprocessing/OsuDifficultyHitObject.cs        | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs
index 448dc140a3..a118d95ff7 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs
@@ -21,12 +21,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
         public OsuHitObject BaseObject { get; }
 
         /// <summary>
-        /// Normalized distance from the end position of the previous <see cref="OsuDifficultyHitObject"/>.
+        /// Normalized distance from the end position of the previous <see cref="OsuDifficultyHitObject"/> to the start position of this <see cref="OsuDifficultyHitObject"/>.
         /// </summary>
         public double JumpDistance { get; private set; }
 
         /// <summary>
-        /// Normalized distance from the start position to the end position of the previous <see cref="OsuDifficultyHitObject"/>.
+        /// Normalized distance between the start and end position of the previous <see cref="OsuDifficultyHitObject"/>.
         /// </summary>
         public double TravelDistance { get; private set; }
 

From d6784c818e02d02c72a86ee3bb5071009b18be63 Mon Sep 17 00:00:00 2001
From: smoogipoo <smoogipoo@smgi.me>
Date: Wed, 10 Oct 2018 11:49:54 +0900
Subject: [PATCH 10/17] Fix jump/travel distances in some scenarios

---
 .../Difficulty/Preprocessing/OsuDifficultyHitObject.cs   | 9 +++++++--
 1 file changed, 7 insertions(+), 2 deletions(-)

diff --git a/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs
index a118d95ff7..0400f080d9 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs
@@ -74,8 +74,13 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
                 lastTravelDistance = lastSlider.LazyTravelDistance;
             }
 
-            JumpDistance = (BaseObject.Position * scalingFactor - lastCursorPosition * scalingFactor).Length;
-            TravelDistance = lastTravelDistance * scalingFactor;
+            // Don't need to jump to reach spinners
+            if (!(BaseObject is Spinner))
+                JumpDistance = (BaseObject.Position * scalingFactor - lastCursorPosition * scalingFactor).Length;
+
+            // Todo: BUG!!! Last slider's travel distance is considered ONLY IF we ourselves are also sliders!
+            if (BaseObject is Slider)
+                TravelDistance = lastTravelDistance * scalingFactor;
         }
 
         private void setTimingValues()

From 03a95113994fa92d8a296370c373acc0cad65cd7 Mon Sep 17 00:00:00 2001
From: smoogipoo <smoogipoo@smgi.me>
Date: Wed, 10 Oct 2018 18:08:46 +0900
Subject: [PATCH 11/17] Apparently stable does use stacking

---
 .../Difficulty/Preprocessing/OsuDifficultyHitObject.cs      | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs
index 0400f080d9..fd36e85f26 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs
@@ -63,7 +63,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
                 scalingFactor *= 1 + smallCircleBonus;
             }
 
-            Vector2 lastCursorPosition = lastObject.Position;
+            Vector2 lastCursorPosition = lastObject.StackedPosition;
             float lastTravelDistance = 0;
 
             var lastSlider = lastObject as Slider;
@@ -76,7 +76,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
 
             // Don't need to jump to reach spinners
             if (!(BaseObject is Spinner))
-                JumpDistance = (BaseObject.Position * scalingFactor - lastCursorPosition * scalingFactor).Length;
+                JumpDistance = (BaseObject.StackedPosition * scalingFactor - lastCursorPosition * scalingFactor).Length;
 
             // Todo: BUG!!! Last slider's travel distance is considered ONLY IF we ourselves are also sliders!
             if (BaseObject is Slider)
@@ -105,7 +105,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
                     progress = progress % 1;
 
                 // ReSharper disable once PossibleInvalidOperationException (bugged in current r# version)
-                var diff = slider.Position + slider.Curve.PositionAt(progress) - slider.LazyEndPosition.Value;
+                var diff = slider.StackedPosition + slider.Curve.PositionAt(progress) - slider.LazyEndPosition.Value;
                 float dist = diff.Length;
 
                 if (dist > approxFollowCircleRadius)

From d8f77feddd705e9fc986b4c4ec3b0b3c4472f273 Mon Sep 17 00:00:00 2001
From: smoogipoo <smoogipoo@smgi.me>
Date: Wed, 10 Oct 2018 18:52:57 +0900
Subject: [PATCH 12/17] Fix using the wrong slider's travel distance

---
 .../Difficulty/Preprocessing/OsuDifficultyHitObject.cs      | 6 ++----
 1 file changed, 2 insertions(+), 4 deletions(-)

diff --git a/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs
index fd36e85f26..1c5b28f407 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs
@@ -64,23 +64,21 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
             }
 
             Vector2 lastCursorPosition = lastObject.StackedPosition;
-            float lastTravelDistance = 0;
 
             var lastSlider = lastObject as Slider;
             if (lastSlider != null)
             {
                 computeSliderCursorPosition(lastSlider);
                 lastCursorPosition = lastSlider.LazyEndPosition ?? lastCursorPosition;
-                lastTravelDistance = lastSlider.LazyTravelDistance;
             }
 
             // Don't need to jump to reach spinners
             if (!(BaseObject is Spinner))
                 JumpDistance = (BaseObject.StackedPosition * scalingFactor - lastCursorPosition * scalingFactor).Length;
 
-            // Todo: BUG!!! Last slider's travel distance is considered ONLY IF we ourselves are also sliders!
+            // Todo: BUG!!! Last slider's travel distance is considered ONLY IF we ourselves are also a slider!
             if (BaseObject is Slider)
-                TravelDistance = lastTravelDistance * scalingFactor;
+                TravelDistance = (lastSlider?.LazyTravelDistance ?? 0) * scalingFactor;
         }
 
         private void setTimingValues()

From 4e37b5c4a7878b11905efd2c4be80ba5e10f33f0 Mon Sep 17 00:00:00 2001
From: smoogipoo <smoogipoo@smgi.me>
Date: Wed, 10 Oct 2018 18:53:54 +0900
Subject: [PATCH 13/17] 50ms cap shouldn't be included in the strain decay

---
 .../Preprocessing/OsuDifficultyHitObject.cs           | 11 +++++++++--
 osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs        |  3 ++-
 osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs      |  2 +-
 3 files changed, 12 insertions(+), 4 deletions(-)

diff --git a/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs
index 1c5b28f407..4905c007b9 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs
@@ -35,6 +35,11 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
         /// </summary>
         public double DeltaTime { get; private set; }
 
+        /// <summary>
+        /// Milliseconds elapsed since the start time of the previous <see cref="OsuDifficultyHitObject"/>, with a minimum of 50ms.
+        /// </summary>
+        public double StrainTime { get; private set; }
+
         private readonly OsuHitObject lastObject;
         private readonly double timeRate;
 
@@ -83,8 +88,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
 
         private void setTimingValues()
         {
-            // Every timing inverval is hard capped at the equivalent of 375 BPM streaming speed as a safety measure.
-            DeltaTime = Math.Max(50, (BaseObject.StartTime - lastObject.StartTime) / timeRate);
+            DeltaTime = (BaseObject.StartTime - lastObject.StartTime) / timeRate;
+
+            // Every strain interval is hard capped at the equivalent of 375 BPM streaming speed as a safety measure
+            StrainTime = Math.Max(50, DeltaTime);
         }
 
         private void computeSliderCursorPosition(Slider slider)
diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs
index 9ffdd280aa..f11b6d66f6 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs
@@ -14,6 +14,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
         protected override double SkillMultiplier => 26.25;
         protected override double StrainDecayBase => 0.15;
 
-        protected override double StrainValueOf(OsuDifficultyHitObject current) => (Math.Pow(current.TravelDistance, 0.99) + Math.Pow(current.JumpDistance, 0.99)) / current.DeltaTime;
+        protected override double StrainValueOf(OsuDifficultyHitObject current)
+            => (Math.Pow(current.TravelDistance, 0.99) + Math.Pow(current.JumpDistance, 0.99)) / current.StrainTime;
     }
 }
diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs
index 501a8e8e01..1cde03624b 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs
@@ -33,7 +33,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
             else
                 speedValue = 0.95;
 
-            return speedValue / current.DeltaTime;
+            return speedValue / current.StrainTime;
         }
     }
 }

From f675c939356a9140c7a4e77b7adbbd602f0e986e Mon Sep 17 00:00:00 2001
From: smoogipoo <smoogipoo@smgi.me>
Date: Wed, 10 Oct 2018 21:37:02 +0900
Subject: [PATCH 14/17] Stably-sort hitobjects

---
 .../Difficulty/Preprocessing/OsuDifficultyBeatmap.cs          | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyBeatmap.cs b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyBeatmap.cs
index 4443a0e66b..24d4677981 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyBeatmap.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyBeatmap.cs
@@ -3,6 +3,7 @@
 
 using System.Collections;
 using System.Collections.Generic;
+using System.Linq;
 using osu.Game.Rulesets.Osu.Objects;
 
 namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
@@ -23,8 +24,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
         {
             // Sort OsuHitObjects by StartTime - they are not correctly ordered in some cases.
             // This should probably happen before the objects reach the difficulty calculator.
-            objects.Sort((a, b) => a.StartTime.CompareTo(b.StartTime));
-            difficultyObjects = createDifficultyObjectEnumerator(objects, timeRate);
+            difficultyObjects = createDifficultyObjectEnumerator(objects.OrderBy(h => h.StartTime).ToList(), timeRate);
         }
 
         /// <summary>

From 7d20efed2c9318b5bd7e1391902714438085a206 Mon Sep 17 00:00:00 2001
From: smoogipoo <smoogipoo@smgi.me>
Date: Thu, 11 Oct 2018 13:53:29 +0900
Subject: [PATCH 15/17] Fix missing stack position

---
 .../Difficulty/Preprocessing/OsuDifficultyHitObject.cs          | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs
index 4905c007b9..ccfcc1ef25 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs
@@ -98,7 +98,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
         {
             if (slider.LazyEndPosition != null)
                 return;
-            slider.LazyEndPosition = slider.Position;
+            slider.LazyEndPosition = slider.StackedPosition;
 
             float approxFollowCircleRadius = (float)(slider.Radius * 3);
             var computeVertex = new Action<double>(t =>

From 0c4403ef16b17a71208537321b4ed6fd80c3a975 Mon Sep 17 00:00:00 2001
From: smoogipoo <smoogipoo@smgi.me>
Date: Thu, 11 Oct 2018 13:53:49 +0900
Subject: [PATCH 16/17] Don't apply version offset during diff calc

---
 .../Beatmaps/Formats/LegacyDifficultyCalculatorBeatmapDecoder.cs | 1 +
 1 file changed, 1 insertion(+)

diff --git a/osu.Game/Beatmaps/Formats/LegacyDifficultyCalculatorBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyDifficultyCalculatorBeatmapDecoder.cs
index 61efd329c0..13a71aac3d 100644
--- a/osu.Game/Beatmaps/Formats/LegacyDifficultyCalculatorBeatmapDecoder.cs
+++ b/osu.Game/Beatmaps/Formats/LegacyDifficultyCalculatorBeatmapDecoder.cs
@@ -18,6 +18,7 @@ namespace osu.Game.Beatmaps.Formats
         public LegacyDifficultyCalculatorBeatmapDecoder(int version = LATEST_VERSION)
             : base(version)
         {
+            ApplyOffsets = false;
         }
 
         public new static void Register()

From 72c8ae8705bae27510e112b311237f3df4ff749a Mon Sep 17 00:00:00 2001
From: smoogipoo <smoogipoo@smgi.me>
Date: Fri, 12 Oct 2018 16:47:18 +0900
Subject: [PATCH 17/17] Port the old stacking algorithm

---
 .../Beatmaps/OsuBeatmapProcessor.cs           | 55 ++++++++++++++++---
 1 file changed, 48 insertions(+), 7 deletions(-)

diff --git a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapProcessor.cs b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapProcessor.cs
index cfb1b0f050..db80948c94 100644
--- a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapProcessor.cs
+++ b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapProcessor.cs
@@ -10,6 +10,8 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
 {
     public class OsuBeatmapProcessor : BeatmapProcessor
     {
+        private const int stack_distance = 3;
+
         public OsuBeatmapProcessor(IBeatmap beatmap)
             : base(beatmap)
         {
@@ -18,17 +20,21 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
         public override void PostProcess()
         {
             base.PostProcess();
-            applyStacking((Beatmap<OsuHitObject>)Beatmap);
+
+            var osuBeatmap = (Beatmap<OsuHitObject>)Beatmap;
+
+            // Reset stacking
+            foreach (var h in osuBeatmap.HitObjects)
+                h.StackHeight = 0;
+
+            if (Beatmap.BeatmapInfo.BeatmapVersion >= 6)
+                applyStacking(osuBeatmap);
+            else
+                applyStackingOld(osuBeatmap);
         }
 
         private void applyStacking(Beatmap<OsuHitObject> beatmap)
         {
-            const int stack_distance = 3;
-
-            // Reset stacking
-            for (int i = 0; i <= beatmap.HitObjects.Count - 1; i++)
-                beatmap.HitObjects[i].StackHeight = 0;
-
             // Extend the end index to include objects they are stacked on
             int extendedEndIndex = beatmap.HitObjects.Count - 1;
             for (int i = beatmap.HitObjects.Count - 1; i >= 0; i--)
@@ -167,5 +173,40 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
                 }
             }
         }
+
+        private void applyStackingOld(Beatmap<OsuHitObject> beatmap)
+        {
+            for (int i = 0; i < beatmap.HitObjects.Count; i++)
+            {
+                OsuHitObject currHitObject = beatmap.HitObjects[i];
+
+                if (currHitObject.StackHeight != 0 && !(currHitObject is Slider))
+                    continue;
+
+                double startTime = (currHitObject as IHasEndTime)?.EndTime ?? currHitObject.StartTime;
+                int sliderStack = 0;
+
+                for (int j = i + 1; j < beatmap.HitObjects.Count; j++)
+                {
+                    double stackThreshold = beatmap.HitObjects[i].TimePreempt * beatmap.BeatmapInfo.StackLeniency;
+
+                    if (beatmap.HitObjects[j].StartTime - stackThreshold > startTime)
+                        break;
+
+                    if (Vector2Extensions.Distance(beatmap.HitObjects[j].Position, currHitObject.Position) < stack_distance)
+                    {
+                        currHitObject.StackHeight++;
+                        startTime = (beatmap.HitObjects[j] as IHasEndTime)?.EndTime ?? beatmap.HitObjects[i].StartTime;
+                    }
+                    else if (Vector2Extensions.Distance(beatmap.HitObjects[j].Position, currHitObject.EndPosition) < stack_distance)
+                    {
+                        //Case for sliders - bump notes down and right, rather than up and left.
+                        sliderStack++;
+                        beatmap.HitObjects[j].StackHeight -= sliderStack;
+                        startTime = (beatmap.HitObjects[j] as IHasEndTime)?.EndTime ?? beatmap.HitObjects[i].StartTime;
+                    }
+                }
+            }
+        }
     }
 }