diff --git a/osu.Game.Rulesets.Catch.Tests/CatchDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Catch.Tests/CatchDifficultyCalculatorTest.cs
index 91bc537902..84f4fd9c99 100644
--- a/osu.Game.Rulesets.Catch.Tests/CatchDifficultyCalculatorTest.cs
+++ b/osu.Game.Rulesets.Catch.Tests/CatchDifficultyCalculatorTest.cs
@@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Catch.Tests
public void Test(double expected, string name)
=> base.Test(expected, name);
- protected override DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => new CatchDifficultyCalculator(new CatchRuleset(), beatmap);
+ protected override LegacyDifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => new CatchLegacyDifficultyCalculator(new CatchRuleset(), beatmap);
protected override Ruleset CreateRuleset() => new CatchRuleset();
}
diff --git a/osu.Game.Rulesets.Catch/CatchRuleset.cs b/osu.Game.Rulesets.Catch/CatchRuleset.cs
index a69070e93e..9b2bbc9bf7 100644
--- a/osu.Game.Rulesets.Catch/CatchRuleset.cs
+++ b/osu.Game.Rulesets.Catch/CatchRuleset.cs
@@ -110,7 +110,7 @@ namespace osu.Game.Rulesets.Catch
public override Drawable CreateIcon() => new SpriteIcon { Icon = FontAwesome.fa_osu_fruits_o };
- public override DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => new CatchDifficultyCalculator(this, beatmap);
+ public override LegacyDifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => new CatchLegacyDifficultyCalculator(this, beatmap);
public override int? LegacyID => 2;
diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchLegacyDifficultyCalculator.cs
similarity index 97%
rename from osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs
rename to osu.Game.Rulesets.Catch/Difficulty/CatchLegacyDifficultyCalculator.cs
index a0b813478d..0a0897d97b 100644
--- a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs
+++ b/osu.Game.Rulesets.Catch/Difficulty/CatchLegacyDifficultyCalculator.cs
@@ -12,7 +12,7 @@ using osu.Game.Rulesets.Catch.UI;
namespace osu.Game.Rulesets.Catch.Difficulty
{
- public class CatchDifficultyCalculator : DifficultyCalculator
+ public class CatchLegacyDifficultyCalculator : LegacyDifficultyCalculator
{
///
/// In milliseconds. For difficulty calculation we will only look at the highest strain value in each time interval of size STRAIN_STEP.
@@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty
private const double star_scaling_factor = 0.145;
- public CatchDifficultyCalculator(Ruleset ruleset, WorkingBeatmap beatmap)
+ public CatchLegacyDifficultyCalculator(Ruleset ruleset, WorkingBeatmap beatmap)
: base(ruleset, beatmap)
{
}
diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Mania.Tests/ManiaDifficultyCalculatorTest.cs
index 05e2df796c..ef660b9ea8 100644
--- a/osu.Game.Rulesets.Mania.Tests/ManiaDifficultyCalculatorTest.cs
+++ b/osu.Game.Rulesets.Mania.Tests/ManiaDifficultyCalculatorTest.cs
@@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Mania.Tests
public void Test(double expected, string name)
=> base.Test(expected, name);
- protected override DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => new ManiaDifficultyCalculator(new ManiaRuleset(), beatmap);
+ protected override LegacyDifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => new ManiaLegacyDifficultyCalculator(new ManiaRuleset(), beatmap);
protected override Ruleset CreateRuleset() => new ManiaRuleset();
}
diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaLegacyDifficultyCalculator.cs
similarity index 97%
rename from osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs
rename to osu.Game.Rulesets.Mania/Difficulty/ManiaLegacyDifficultyCalculator.cs
index b8588dbce2..02b03aca5d 100644
--- a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs
+++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaLegacyDifficultyCalculator.cs
@@ -13,7 +13,7 @@ using osu.Game.Rulesets.Mods;
namespace osu.Game.Rulesets.Mania.Difficulty
{
- internal class ManiaDifficultyCalculator : DifficultyCalculator
+ internal class ManiaLegacyDifficultyCalculator : LegacyDifficultyCalculator
{
private const double star_scaling_factor = 0.018;
@@ -31,7 +31,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty
private readonly bool isForCurrentRuleset;
- public ManiaDifficultyCalculator(Ruleset ruleset, WorkingBeatmap beatmap)
+ public ManiaLegacyDifficultyCalculator(Ruleset ruleset, WorkingBeatmap beatmap)
: base(ruleset, beatmap)
{
isForCurrentRuleset = beatmap.BeatmapInfo.Ruleset.Equals(ruleset.RulesetInfo);
diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs
index 57728dd134..7a2a539a9d 100644
--- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs
+++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs
@@ -156,7 +156,7 @@ namespace osu.Game.Rulesets.Mania
public override Drawable CreateIcon() => new SpriteIcon { Icon = FontAwesome.fa_osu_mania_o };
- public override DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => new ManiaDifficultyCalculator(this, beatmap);
+ public override LegacyDifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => new ManiaLegacyDifficultyCalculator(this, beatmap);
public override int? LegacyID => 3;
diff --git a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs
index e55dc1f902..cc46ec7be3 100644
--- a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs
+++ b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs
@@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Osu.Tests
public void Test(double expected, string name)
=> base.Test(expected, name);
- protected override DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => new OsuDifficultyCalculator(new OsuRuleset(), beatmap);
+ protected override LegacyDifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => new OsuLegacyDifficultyCalculator(new OsuRuleset(), beatmap);
protected override Ruleset CreateRuleset() => new OsuRuleset();
}
diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuLegacyDifficultyCalculator.cs
similarity index 95%
rename from osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs
rename to osu.Game.Rulesets.Osu/Difficulty/OsuLegacyDifficultyCalculator.cs
index 3d0a1dc266..d01f75df6b 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/OsuLegacyDifficultyCalculator.cs
@@ -13,12 +13,12 @@ using osu.Game.Rulesets.Osu.Objects;
namespace osu.Game.Rulesets.Osu.Difficulty
{
- public class OsuDifficultyCalculator : DifficultyCalculator
+ public class OsuLegacyDifficultyCalculator : LegacyDifficultyCalculator
{
private const int section_length = 400;
private const double difficulty_multiplier = 0.0675;
- public OsuDifficultyCalculator(Ruleset ruleset, WorkingBeatmap beatmap)
+ public OsuLegacyDifficultyCalculator(Ruleset ruleset, WorkingBeatmap beatmap)
: base(ruleset, beatmap)
{
}
diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs
index 12d0a28a8f..6fa1532580 100644
--- a/osu.Game.Rulesets.Osu/OsuRuleset.cs
+++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs
@@ -132,7 +132,7 @@ namespace osu.Game.Rulesets.Osu
public override Drawable CreateIcon() => new SpriteIcon { Icon = FontAwesome.fa_osu_osu_o };
- public override DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => new OsuDifficultyCalculator(this, beatmap);
+ public override LegacyDifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => new OsuLegacyDifficultyCalculator(this, beatmap);
public override PerformanceCalculator CreatePerformanceCalculator(WorkingBeatmap beatmap, ScoreInfo score) => new OsuPerformanceCalculator(this, beatmap, score);
diff --git a/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs
index 299f84fb1f..e00f3da0b7 100644
--- a/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs
+++ b/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs
@@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
public void Test(double expected, string name)
=> base.Test(expected, name);
- protected override DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => new TaikoDifficultyCalculator(new TaikoRuleset(), beatmap);
+ protected override LegacyDifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => new TaikoLegacyDifficultyCalculator(new TaikoRuleset(), beatmap);
protected override Ruleset CreateRuleset() => new TaikoRuleset();
}
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoLegacyDifficultyCalculator.cs
similarity index 97%
rename from osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs
rename to osu.Game.Rulesets.Taiko/Difficulty/TaikoLegacyDifficultyCalculator.cs
index 2322446666..650b367e34 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoLegacyDifficultyCalculator.cs
@@ -12,7 +12,7 @@ using osu.Game.Rulesets.Taiko.Objects;
namespace osu.Game.Rulesets.Taiko.Difficulty
{
- internal class TaikoDifficultyCalculator : DifficultyCalculator
+ internal class TaikoLegacyDifficultyCalculator : LegacyDifficultyCalculator
{
private const double star_scaling_factor = 0.04125;
@@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
///
private const double decay_weight = 0.9;
- public TaikoDifficultyCalculator(Ruleset ruleset, WorkingBeatmap beatmap)
+ public TaikoLegacyDifficultyCalculator(Ruleset ruleset, WorkingBeatmap beatmap)
: base(ruleset, beatmap)
{
}
diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs
index 7851a2f919..77a53858fe 100644
--- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs
+++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs
@@ -110,7 +110,7 @@ namespace osu.Game.Rulesets.Taiko
public override Drawable CreateIcon() => new SpriteIcon { Icon = FontAwesome.fa_osu_taiko_o };
- public override DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => new TaikoDifficultyCalculator(this, beatmap);
+ public override LegacyDifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => new TaikoLegacyDifficultyCalculator(this, beatmap);
public override PerformanceCalculator CreatePerformanceCalculator(WorkingBeatmap beatmap, ScoreInfo score) => new TaikoPerformanceCalculator(this, beatmap, score);
diff --git a/osu.Game.Tests/NonVisual/DifficultyAdjustmentModCombinationsTest.cs b/osu.Game.Tests/NonVisual/DifficultyAdjustmentModCombinationsTest.cs
index 795c2244a2..f57f25e1ff 100644
--- a/osu.Game.Tests/NonVisual/DifficultyAdjustmentModCombinationsTest.cs
+++ b/osu.Game.Tests/NonVisual/DifficultyAdjustmentModCombinationsTest.cs
@@ -15,7 +15,7 @@ namespace osu.Game.Tests.NonVisual
[Test]
public void TestNoMods()
{
- var combinations = new TestDifficultyCalculator().CreateDifficultyAdjustmentModCombinations();
+ var combinations = new TestLegacyDifficultyCalculator().CreateDifficultyAdjustmentModCombinations();
Assert.AreEqual(1, combinations.Length);
Assert.IsTrue(combinations[0] is ModNoMod);
@@ -24,7 +24,7 @@ namespace osu.Game.Tests.NonVisual
[Test]
public void TestSingleMod()
{
- var combinations = new TestDifficultyCalculator(new ModA()).CreateDifficultyAdjustmentModCombinations();
+ var combinations = new TestLegacyDifficultyCalculator(new ModA()).CreateDifficultyAdjustmentModCombinations();
Assert.AreEqual(2, combinations.Length);
Assert.IsTrue(combinations[0] is ModNoMod);
@@ -34,7 +34,7 @@ namespace osu.Game.Tests.NonVisual
[Test]
public void TestDoubleMod()
{
- var combinations = new TestDifficultyCalculator(new ModA(), new ModB()).CreateDifficultyAdjustmentModCombinations();
+ var combinations = new TestLegacyDifficultyCalculator(new ModA(), new ModB()).CreateDifficultyAdjustmentModCombinations();
Assert.AreEqual(4, combinations.Length);
Assert.IsTrue(combinations[0] is ModNoMod);
@@ -49,7 +49,7 @@ namespace osu.Game.Tests.NonVisual
[Test]
public void TestIncompatibleMods()
{
- var combinations = new TestDifficultyCalculator(new ModA(), new ModIncompatibleWithA()).CreateDifficultyAdjustmentModCombinations();
+ var combinations = new TestLegacyDifficultyCalculator(new ModA(), new ModIncompatibleWithA()).CreateDifficultyAdjustmentModCombinations();
Assert.AreEqual(3, combinations.Length);
Assert.IsTrue(combinations[0] is ModNoMod);
@@ -60,7 +60,7 @@ namespace osu.Game.Tests.NonVisual
[Test]
public void TestDoubleIncompatibleMods()
{
- var combinations = new TestDifficultyCalculator(new ModA(), new ModB(), new ModIncompatibleWithA(), new ModIncompatibleWithAAndB()).CreateDifficultyAdjustmentModCombinations();
+ var combinations = new TestLegacyDifficultyCalculator(new ModA(), new ModB(), new ModIncompatibleWithA(), new ModIncompatibleWithAAndB()).CreateDifficultyAdjustmentModCombinations();
Assert.AreEqual(8, combinations.Length);
Assert.IsTrue(combinations[0] is ModNoMod);
@@ -83,7 +83,7 @@ namespace osu.Game.Tests.NonVisual
[Test]
public void TestIncompatibleThroughBaseType()
{
- var combinations = new TestDifficultyCalculator(new ModAofA(), new ModIncompatibleWithAofA()).CreateDifficultyAdjustmentModCombinations();
+ var combinations = new TestLegacyDifficultyCalculator(new ModAofA(), new ModIncompatibleWithAofA()).CreateDifficultyAdjustmentModCombinations();
Assert.AreEqual(3, combinations.Length);
Assert.IsTrue(combinations[0] is ModNoMod);
@@ -136,9 +136,9 @@ namespace osu.Game.Tests.NonVisual
public override Type[] IncompatibleMods => new[] { typeof(ModA), typeof(ModB) };
}
- private class TestDifficultyCalculator : DifficultyCalculator
+ private class TestLegacyDifficultyCalculator : LegacyDifficultyCalculator
{
- public TestDifficultyCalculator(params Mod[] mods)
+ public TestLegacyDifficultyCalculator(params Mod[] mods)
: base(null, null)
{
DifficultyAdjustmentMods = mods;
diff --git a/osu.Game/Beatmaps/DummyWorkingBeatmap.cs b/osu.Game/Beatmaps/DummyWorkingBeatmap.cs
index 0aa1697bf8..eb9e221ca4 100644
--- a/osu.Game/Beatmaps/DummyWorkingBeatmap.cs
+++ b/osu.Game/Beatmaps/DummyWorkingBeatmap.cs
@@ -54,7 +54,7 @@ namespace osu.Game.Beatmaps
public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new DummyBeatmapConverter { Beatmap = beatmap };
- public override DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => null;
+ public override LegacyDifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => null;
public override string Description => "dummy";
diff --git a/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs b/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs
index d7654462d5..b1a88b8abd 100644
--- a/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs
+++ b/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs
@@ -8,7 +8,13 @@ namespace osu.Game.Rulesets.Difficulty
public class DifficultyAttributes
{
public readonly Mod[] Mods;
- public readonly double StarRating;
+
+ public double StarRating;
+
+ public DifficultyAttributes(Mod[] mods)
+ {
+ Mods = mods;
+ }
public DifficultyAttributes(Mod[] mods, double starRating)
{
diff --git a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs
index 818d24508a..d7ae527bb1 100644
--- a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs
+++ b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs
@@ -1,56 +1,67 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
-using osu.Framework.Extensions.IEnumerableExtensions;
-using osu.Framework.Timing;
using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Difficulty.Preprocessing;
+using osu.Game.Rulesets.Difficulty.Skills;
using osu.Game.Rulesets.Mods;
namespace osu.Game.Rulesets.Difficulty
{
- public abstract class DifficultyCalculator
+ public abstract class DifficultyCalculator : LegacyDifficultyCalculator
{
- private readonly Ruleset ruleset;
- private readonly WorkingBeatmap beatmap;
+ ///
+ /// The length of each strain section.
+ ///
+ protected virtual int SectionLength => 400;
protected DifficultyCalculator(Ruleset ruleset, WorkingBeatmap beatmap)
+ : base(ruleset, beatmap)
{
- this.ruleset = ruleset;
- this.beatmap = beatmap;
}
- ///
- /// Calculates the difficulty of the beatmap using a specific mod combination.
- ///
- /// The mods that should be applied to the beatmap.
- /// A structure describing the difficulty of the beatmap.
- public DifficultyAttributes Calculate(params Mod[] mods)
+ protected override DifficultyAttributes Calculate(IBeatmap beatmap, Mod[] mods, double timeRate)
{
- beatmap.Mods.Value = mods;
- IBeatmap playableBeatmap = beatmap.GetPlayableBeatmap(ruleset.RulesetInfo);
+ var attributes = CreateDifficultyAttributes(mods);
- var clock = new StopwatchClock();
- mods.OfType().ForEach(m => m.ApplyToClock(clock));
+ if (!beatmap.HitObjects.Any())
+ return attributes;
- return Calculate(playableBeatmap, mods, clock.Rate);
- }
+ var difficultyHitObjects = CreateDifficultyHitObjects(beatmap, timeRate).OrderBy(h => h.BaseObject.StartTime).ToList();
+ var skills = CreateSkills();
- ///
- /// Calculates the difficulty of the beatmap using all mod combinations applicable to the beatmap.
- ///
- /// A collection of structures describing the difficulty of the beatmap for each mod combination.
- public IEnumerable CalculateAll()
- {
- foreach (var combination in CreateDifficultyAdjustmentModCombinations())
+ double sectionLength = SectionLength * timeRate;
+
+ // The first object doesn't generate a strain, so we begin with an incremented section end
+ double currentSectionEnd = Math.Ceiling(beatmap.HitObjects.First().StartTime / sectionLength) * sectionLength;
+
+ foreach (DifficultyHitObject h in difficultyHitObjects)
{
- if (combination is MultiMod multi)
- yield return Calculate(multi.Mods);
- else
- yield return Calculate(combination);
+ while (h.BaseObject.StartTime > currentSectionEnd)
+ {
+ foreach (Skill s in skills)
+ {
+ s.SaveCurrentPeak();
+ s.StartNewSectionFrom(currentSectionEnd);
+ }
+
+ currentSectionEnd += sectionLength;
+ }
+
+ foreach (Skill s in skills)
+ s.Process(h);
}
+
+ // The peak strain will not be saved for the last section in the above loop
+ foreach (Skill s in skills)
+ s.SaveCurrentPeak();
+
+ PopulateAttributes(attributes, beatmap, skills, timeRate);
+
+ return attributes;
}
///
@@ -96,12 +107,33 @@ namespace osu.Game.Rulesets.Difficulty
protected virtual Mod[] DifficultyAdjustmentMods => Array.Empty();
///
- /// Calculates the difficulty of a using a specific combination.
+ /// Populates after difficulty has been processed.
///
- /// The to compute the difficulty for.
- /// The s that should be applied.
+ /// The to populate with information about the difficulty of .
+ /// The whose difficulty was processed.
+ /// The skills which processed the difficulty.
/// The rate of time in .
- /// A structure containing the difficulty attributes.
- protected abstract DifficultyAttributes Calculate(IBeatmap beatmap, Mod[] mods, double timeRate);
+ protected abstract void PopulateAttributes(DifficultyAttributes attributes, IBeatmap beatmap, Skill[] skills, double timeRate);
+
+ ///
+ /// Enumerates s to be processed from s in the .
+ ///
+ /// The providing the s to enumerate.
+ /// The rate of time in .
+ /// The enumerated s.
+ protected abstract IEnumerable CreateDifficultyHitObjects(IBeatmap beatmap, double timeRate);
+
+ ///
+ /// Creates the s to calculate the difficulty of s.
+ ///
+ /// The s.
+ protected abstract Skill[] CreateSkills();
+
+ ///
+ /// Creates an empty .
+ ///
+ /// The s which difficulty is being processed with.
+ /// The empty .
+ protected abstract DifficultyAttributes CreateDifficultyAttributes(Mod[] mods);
}
}
diff --git a/osu.Game/Rulesets/Difficulty/LegacyDifficultyCalculator.cs b/osu.Game/Rulesets/Difficulty/LegacyDifficultyCalculator.cs
new file mode 100644
index 0000000000..15565ef847
--- /dev/null
+++ b/osu.Game/Rulesets/Difficulty/LegacyDifficultyCalculator.cs
@@ -0,0 +1,107 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using osu.Framework.Extensions.IEnumerableExtensions;
+using osu.Framework.Timing;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Mods;
+
+namespace osu.Game.Rulesets.Difficulty
+{
+ public abstract class LegacyDifficultyCalculator
+ {
+ private readonly Ruleset ruleset;
+ private readonly WorkingBeatmap beatmap;
+
+ protected LegacyDifficultyCalculator(Ruleset ruleset, WorkingBeatmap beatmap)
+ {
+ this.ruleset = ruleset;
+ this.beatmap = beatmap;
+ }
+
+ ///
+ /// Calculates the difficulty of the beatmap using a specific mod combination.
+ ///
+ /// The mods that should be applied to the beatmap.
+ /// A structure describing the difficulty of the beatmap.
+ public DifficultyAttributes Calculate(params Mod[] mods)
+ {
+ beatmap.Mods.Value = mods;
+ IBeatmap playableBeatmap = beatmap.GetPlayableBeatmap(ruleset.RulesetInfo);
+
+ var clock = new StopwatchClock();
+ mods.OfType().ForEach(m => m.ApplyToClock(clock));
+
+ return Calculate(playableBeatmap, mods, clock.Rate);
+ }
+
+ ///
+ /// Calculates the difficulty of the beatmap using all mod combinations applicable to the beatmap.
+ ///
+ /// A collection of structures describing the difficulty of the beatmap for each mod combination.
+ public IEnumerable CalculateAll()
+ {
+ foreach (var combination in CreateDifficultyAdjustmentModCombinations())
+ {
+ if (combination is MultiMod multi)
+ yield return Calculate(multi.Mods);
+ else
+ yield return Calculate(combination);
+ }
+ }
+
+ ///
+ /// Creates all combinations which adjust the difficulty.
+ ///
+ public Mod[] CreateDifficultyAdjustmentModCombinations()
+ {
+ return createDifficultyAdjustmentModCombinations(Enumerable.Empty(), DifficultyAdjustmentMods).ToArray();
+
+ IEnumerable createDifficultyAdjustmentModCombinations(IEnumerable currentSet, Mod[] adjustmentSet, int currentSetCount = 0, int adjustmentSetStart = 0)
+ {
+ switch (currentSetCount)
+ {
+ case 0:
+ // Initial-case: Empty current set
+ yield return new ModNoMod();
+ break;
+ case 1:
+ yield return currentSet.Single();
+ break;
+ default:
+ yield return new MultiMod(currentSet.ToArray());
+ break;
+ }
+
+ // Apply mods in the adjustment set recursively. Using the entire adjustment set would result in duplicate multi-mod mod
+ // combinations in further recursions, so a moving subset is used to eliminate this effect
+ for (int i = adjustmentSetStart; i < adjustmentSet.Length; i++)
+ {
+ var adjustmentMod = adjustmentSet[i];
+ if (currentSet.Any(c => c.IncompatibleMods.Any(m => m.IsInstanceOfType(adjustmentMod))))
+ continue;
+
+ foreach (var combo in createDifficultyAdjustmentModCombinations(currentSet.Append(adjustmentMod), adjustmentSet, currentSetCount + 1, i + 1))
+ yield return combo;
+ }
+ }
+ }
+
+ ///
+ /// Retrieves all s which adjust the difficulty.
+ ///
+ protected virtual Mod[] DifficultyAdjustmentMods => Array.Empty();
+
+ ///
+ /// Calculates the difficulty of a using a specific combination.
+ ///
+ /// The to compute the difficulty for.
+ /// The s that should be applied.
+ /// The rate of time in .
+ /// A structure containing the difficulty attributes.
+ protected abstract DifficultyAttributes Calculate(IBeatmap beatmap, Mod[] mods, double timeRate);
+ }
+}
diff --git a/osu.Game/Rulesets/Difficulty/Preprocessing/DifficultyHitObject.cs b/osu.Game/Rulesets/Difficulty/Preprocessing/DifficultyHitObject.cs
new file mode 100644
index 0000000000..77c9b7e47f
--- /dev/null
+++ b/osu.Game/Rulesets/Difficulty/Preprocessing/DifficultyHitObject.cs
@@ -0,0 +1,32 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Game.Rulesets.Objects;
+
+namespace osu.Game.Rulesets.Difficulty.Preprocessing
+{
+ public class DifficultyHitObject
+ {
+ ///
+ /// Milliseconds elapsed since the of the previous .
+ ///
+ public double DeltaTime { get; private set; }
+
+ ///
+ /// The this refers to.
+ ///
+ public readonly HitObject BaseObject;
+
+ ///
+ /// The previous to .
+ ///
+ public readonly HitObject LastObject;
+
+ public DifficultyHitObject(HitObject hitObject, HitObject lastObject, double timeRate)
+ {
+ BaseObject = hitObject;
+ LastObject = lastObject;
+ DeltaTime = (hitObject.StartTime - lastObject.StartTime) / timeRate;
+ }
+ }
+}
diff --git a/osu.Game/Rulesets/Difficulty/Skills/Skill.cs b/osu.Game/Rulesets/Difficulty/Skills/Skill.cs
new file mode 100644
index 0000000000..fa7aa8f637
--- /dev/null
+++ b/osu.Game/Rulesets/Difficulty/Skills/Skill.cs
@@ -0,0 +1,103 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Collections.Generic;
+using osu.Game.Rulesets.Difficulty.Preprocessing;
+using osu.Game.Rulesets.Difficulty.Utils;
+
+namespace osu.Game.Rulesets.Difficulty.Skills
+{
+ ///
+ /// Used to processes strain values of s, keep track of strain levels caused by the processed objects
+ /// and to calculate a final difficulty value representing the difficulty of hitting all the processed objects.
+ ///
+ public abstract class Skill
+ {
+ ///
+ /// Strain values are multiplied by this number for the given skill. Used to balance the value of different skills between each other.
+ ///
+ protected abstract double SkillMultiplier { get; }
+
+ ///
+ /// Determines how quickly strain decays for the given skill.
+ /// For example a value of 0.15 indicates that strain decays to 15% of its original value in one second.
+ ///
+ protected abstract double StrainDecayBase { get; }
+
+ ///
+ /// The weight by which each strain value decays.
+ ///
+ protected virtual double DecayWeight => 0.9;
+
+ ///
+ /// s that were processed previously. They can affect the strain values of the following objects.
+ ///
+ protected readonly History Previous = new History(2); // Contained objects not used yet
+
+ private double currentStrain = 1; // We keep track of the strain level at all times throughout the beatmap.
+ private double currentSectionPeak = 1; // We also keep track of the peak strain level in the current section.
+ private readonly List strainPeaks = new List();
+
+ ///
+ /// Process a and update current strain values accordingly.
+ ///
+ public void Process(DifficultyHitObject current)
+ {
+ currentStrain *= strainDecay(current.DeltaTime);
+ currentStrain += StrainValueOf(current) * SkillMultiplier;
+
+ currentSectionPeak = Math.Max(currentStrain, currentSectionPeak);
+
+ Previous.Push(current);
+ }
+
+ ///
+ /// Saves the current peak strain level to the list of strain peaks, which will be used to calculate an overall difficulty.
+ ///
+ public void SaveCurrentPeak()
+ {
+ if (Previous.Count > 0)
+ strainPeaks.Add(currentSectionPeak);
+ }
+
+ ///
+ /// Sets the initial strain level for a new section.
+ ///
+ /// The beginning of the new section in milliseconds.
+ public void StartNewSectionFrom(double offset)
+ {
+ // The maximum strain of the new section is not zero by default, strain decays as usual regardless of section boundaries.
+ // This means we need to capture the strain level at the beginning of the new section, and use that as the initial peak level.
+ if (Previous.Count > 0)
+ currentSectionPeak = currentStrain * strainDecay(offset - Previous[0].BaseObject.StartTime);
+ }
+
+ ///
+ /// Returns the calculated difficulty value representing all processed s.
+ ///
+ public double DifficultyValue()
+ {
+ strainPeaks.Sort((a, b) => b.CompareTo(a)); // Sort from highest to lowest strain.
+
+ double difficulty = 0;
+ double weight = 1;
+
+ // Difficulty is the weighted sum of the highest strains from every section.
+ foreach (double strain in strainPeaks)
+ {
+ difficulty += strain * weight;
+ weight *= DecayWeight;
+ }
+
+ return difficulty;
+ }
+
+ ///
+ /// Calculates the strain value of a . This value is affected by previously processed objects.
+ ///
+ protected abstract double StrainValueOf(DifficultyHitObject current);
+
+ private double strainDecay(double ms) => Math.Pow(StrainDecayBase, ms / 1000);
+ }
+}
diff --git a/osu.Game/Rulesets/Difficulty/Utils/History.cs b/osu.Game/Rulesets/Difficulty/Utils/History.cs
new file mode 100644
index 0000000000..d6647d5119
--- /dev/null
+++ b/osu.Game/Rulesets/Difficulty/Utils/History.cs
@@ -0,0 +1,86 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Collections;
+using System.Collections.Generic;
+
+namespace osu.Game.Rulesets.Difficulty.Utils
+{
+ ///
+ /// An indexed stack with Push() only, which disposes items at the bottom after the capacity is full.
+ /// Indexing starts at the top of the stack.
+ ///
+ public class History : IEnumerable
+ {
+ public int Count { get; private set; }
+
+ private readonly T[] array;
+ private readonly int capacity;
+ private int marker; // Marks the position of the most recently added item.
+
+ ///
+ /// Initializes a new instance of the History class that is empty and has the specified capacity.
+ ///
+ /// The number of items the History can hold.
+ public History(int capacity)
+ {
+ if (capacity < 0)
+ throw new ArgumentOutOfRangeException();
+
+ this.capacity = capacity;
+ array = new T[capacity];
+ marker = capacity; // Set marker to the end of the array, outside of the indexed range by one.
+ }
+
+ ///
+ /// The most recently added item is returned at index 0.
+ ///
+ public T this[int i]
+ {
+ get
+ {
+ if (i < 0 || i > Count - 1)
+ throw new IndexOutOfRangeException();
+
+ i += marker;
+ if (i > capacity - 1)
+ i -= capacity;
+
+ return array[i];
+ }
+ }
+
+ ///
+ /// Adds the item as the most recent one in the history.
+ /// The oldest item is disposed if the history is full.
+ ///
+ public void Push(T item) // Overwrite the oldest item instead of shifting every item by one with every addition.
+ {
+ if (marker == 0)
+ marker = capacity - 1;
+ else
+ --marker;
+
+ array[marker] = item;
+
+ if (Count < capacity)
+ ++Count;
+ }
+
+ ///
+ /// Returns an enumerator which enumerates items in the history starting from the most recently added one.
+ ///
+ public IEnumerator GetEnumerator()
+ {
+ for (int i = marker; i < capacity; ++i)
+ yield return array[i];
+
+ if (Count == capacity)
+ for (int i = 0; i < marker; ++i)
+ yield return array[i];
+ }
+
+ IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
+ }
+}
diff --git a/osu.Game/Rulesets/Ruleset.cs b/osu.Game/Rulesets/Ruleset.cs
index ffab0abebf..75643a85dc 100644
--- a/osu.Game/Rulesets/Ruleset.cs
+++ b/osu.Game/Rulesets/Ruleset.cs
@@ -71,7 +71,7 @@ namespace osu.Game.Rulesets
/// The .
public virtual IBeatmapProcessor CreateBeatmapProcessor(IBeatmap beatmap) => null;
- public abstract DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap);
+ public abstract LegacyDifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap);
public virtual PerformanceCalculator CreatePerformanceCalculator(WorkingBeatmap beatmap, ScoreInfo score) => null;
diff --git a/osu.Game/Tests/Beatmaps/DifficultyCalculatorTest.cs b/osu.Game/Tests/Beatmaps/DifficultyCalculatorTest.cs
index 108fa8ff71..85ae1958ef 100644
--- a/osu.Game/Tests/Beatmaps/DifficultyCalculatorTest.cs
+++ b/osu.Game/Tests/Beatmaps/DifficultyCalculatorTest.cs
@@ -44,7 +44,7 @@ namespace osu.Game.Tests.Beatmaps
return Assembly.LoadFrom(Path.Combine(localPath, $"{ResourceAssembly}.dll")).GetManifestResourceStream($@"{ResourceAssembly}.Resources.{name}");
}
- protected abstract DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap);
+ protected abstract LegacyDifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap);
protected abstract Ruleset CreateRuleset();
}