diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs
index deefeb915c..0bbf1d3df6 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs
@@ -75,6 +75,15 @@ namespace osu.Game.Rulesets.Osu.Difficulty
[JsonProperty("speed_difficult_strain_count")]
public double SpeedDifficultStrainCount { get; set; }
+ [JsonProperty("slider_nested_score_per_object")]
+ public double SliderNestedScorePerObject { get; set; }
+
+ [JsonProperty("legacy_score_base_multiplier")]
+ public double LegacyScoreBaseMultiplier { get; set; }
+
+ [JsonProperty("maximum_legacy_combo_score")]
+ public double MaximumLegacyComboScore { get; set; }
+
///
/// The beatmap's drain rate. This doesn't scale with rate-adjusting mods.
///
@@ -115,6 +124,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty
yield return (ATTRIB_ID_AIM_DIFFICULT_SLIDER_COUNT, AimDifficultSliderCount);
yield return (ATTRIB_ID_AIM_TOP_WEIGHTED_SLIDER_FACTOR, AimTopWeightedSliderFactor);
yield return (ATTRIB_ID_SPEED_TOP_WEIGHTED_SLIDER_FACTOR, SpeedTopWeightedSliderFactor);
+ yield return (ATTRIB_ID_SLIDER_NESTED_SCORE_PER_OBJECT, SliderNestedScorePerObject);
+ yield return (ATTRIB_ID_LEGACY_SCORE_BASE_MULTIPLIER, LegacyScoreBaseMultiplier);
+ yield return (ATTRIB_ID_MAXIMUM_LEGACY_COMBO_SCORE, MaximumLegacyComboScore);
}
public override void FromDatabaseAttributes(IReadOnlyDictionary values, IBeatmapOnlineInfo onlineInfo)
@@ -132,6 +144,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty
AimDifficultSliderCount = values[ATTRIB_ID_AIM_DIFFICULT_SLIDER_COUNT];
AimTopWeightedSliderFactor = values[ATTRIB_ID_AIM_TOP_WEIGHTED_SLIDER_FACTOR];
SpeedTopWeightedSliderFactor = values[ATTRIB_ID_SPEED_TOP_WEIGHTED_SLIDER_FACTOR];
+ SliderNestedScorePerObject = values[ATTRIB_ID_SLIDER_NESTED_SCORE_PER_OBJECT];
+ LegacyScoreBaseMultiplier = values[ATTRIB_ID_LEGACY_SCORE_BASE_MULTIPLIER];
+ MaximumLegacyComboScore = values[ATTRIB_ID_MAXIMUM_LEGACY_COMBO_SCORE];
DrainRate = onlineInfo.DrainRate;
HitCircleCount = onlineInfo.CircleCount;
SliderCount = onlineInfo.SliderCount;
diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs
index fa142e4429..7c8de87884 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs
@@ -11,6 +11,7 @@ using osu.Game.Rulesets.Difficulty.Skills;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
using osu.Game.Rulesets.Osu.Difficulty.Skills;
+using osu.Game.Rulesets.Osu.Difficulty.Utils;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Scoring;
@@ -112,6 +113,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty
? Math.Cbrt(multiplier) * star_rating_multiplier * (Math.Cbrt(100000 / Math.Pow(2, 1 / 1.1) * basePerformance) + 4)
: 0;
+ double sliderNestedScorePerObject = LegacyScoreUtils.CalculateSliderNestedScorePerObject(beatmap, totalHits);
+ double legacyScoreBaseMultiplier = LegacyScoreUtils.CalculateDifficultyPeppyStars(beatmap);
+
+ var simulator = new OsuLegacyScoreSimulator();
+ var scoreAttributes = simulator.Simulate(WorkingBeatmap, beatmap);
+
OsuDifficultyAttributes attributes = new OsuDifficultyAttributes
{
StarRating = starRating,
@@ -131,6 +138,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty
HitCircleCount = hitCircleCount,
SliderCount = sliderCount,
SpinnerCount = spinnerCount,
+ SliderNestedScorePerObject = sliderNestedScorePerObject,
+ LegacyScoreBaseMultiplier = legacyScoreBaseMultiplier,
+ MaximumLegacyComboScore = scoreAttributes.ComboScore
};
return attributes;
diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuLegacyScoreMissCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuLegacyScoreMissCalculator.cs
new file mode 100644
index 0000000000..53837b78a0
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Difficulty/OsuLegacyScoreMissCalculator.cs
@@ -0,0 +1,187 @@
+// 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.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Osu.Mods;
+using osu.Game.Rulesets.Scoring;
+using osu.Game.Scoring;
+
+namespace osu.Game.Rulesets.Osu.Difficulty
+{
+ public class OsuLegacyScoreMissCalculator
+ {
+ private readonly ScoreInfo score;
+ private readonly OsuDifficultyAttributes attributes;
+
+ public OsuLegacyScoreMissCalculator(ScoreInfo scoreInfo, OsuDifficultyAttributes attributes)
+ {
+ score = scoreInfo;
+ this.attributes = attributes;
+ }
+
+ public double Calculate()
+ {
+ if (attributes.MaxCombo == 0 || score.LegacyTotalScore == null)
+ return 0;
+
+ double scoreV1Multiplier = attributes.LegacyScoreBaseMultiplier * getLegacyScoreMultiplier();
+ double relevantComboPerObject = calculateRelevantScoreComboPerObject();
+
+ double maximumMissCount = calculateMaximumComboBasedMissCount();
+
+ double scoreObtainedDuringMaxCombo = calculateScoreAtCombo(score.MaxCombo, relevantComboPerObject, scoreV1Multiplier);
+ double remainingScore = score.LegacyTotalScore.Value - scoreObtainedDuringMaxCombo;
+
+ if (remainingScore <= 0)
+ return maximumMissCount;
+
+ double remainingCombo = attributes.MaxCombo - score.MaxCombo;
+ double expectedRemainingScore = calculateScoreAtCombo(remainingCombo, relevantComboPerObject, scoreV1Multiplier);
+
+ double scoreBasedMissCount = expectedRemainingScore / remainingScore;
+
+ // If there's less then one miss detected - let combo-based miss count decide if this is FC or not
+ scoreBasedMissCount = Math.Max(scoreBasedMissCount, 1);
+
+ // Cap result by very harsh version of combo-based miss count
+ return Math.Min(scoreBasedMissCount, maximumMissCount);
+ }
+
+ ///
+ /// Calculates the amount of score that would be achieved at a given combo.
+ ///
+ private double calculateScoreAtCombo(double combo, double relevantComboPerObject, double scoreV1Multiplier)
+ {
+ int countGreat = score.Statistics.GetValueOrDefault(HitResult.Great);
+ int countOk = score.Statistics.GetValueOrDefault(HitResult.Ok);
+ int countMeh = score.Statistics.GetValueOrDefault(HitResult.Meh);
+ int countMiss = score.Statistics.GetValueOrDefault(HitResult.Miss);
+
+ int totalHits = countGreat + countOk + countMeh + countMiss;
+
+ double estimatedObjects = combo / relevantComboPerObject - 1;
+
+ // The combo portion of ScoreV1 follows arithmetic progression
+ // Therefore, we calculate the combo portion of score using the combo per object and our current combo.
+ double comboScore = relevantComboPerObject > 0 ? (2 * (relevantComboPerObject - 1) + (estimatedObjects - 1) * relevantComboPerObject) * estimatedObjects / 2 : 0;
+
+ // We then apply the accuracy and ScoreV1 multipliers to the resulting score.
+ comboScore *= score.Accuracy * 300 / 25 * scoreV1Multiplier;
+
+ double objectsHit = (totalHits - countMiss) * combo / attributes.MaxCombo;
+
+ // Score also has a non-combo portion we need to create the final score value.
+ double nonComboScore = (300 + attributes.SliderNestedScorePerObject) * score.Accuracy * objectsHit;
+
+ return comboScore + nonComboScore;
+ }
+
+ ///
+ /// Calculates the relevant combo per object for legacy score.
+ /// This assumes a uniform distribution for circles and sliders.
+ /// This handles cases where objects (such as buzz sliders) do not fit a normal arithmetic progression model.
+ ///
+ private double calculateRelevantScoreComboPerObject()
+ {
+ double comboScore = attributes.MaximumLegacyComboScore;
+
+ // We then reverse apply the ScoreV1 multipliers to get the raw value.
+ comboScore /= 300.0 / 25.0 * attributes.LegacyScoreBaseMultiplier;
+
+ // Reverse the arithmetic progression to work out the amount of combo per object based on the score.
+ double result = (attributes.MaxCombo - 2) * attributes.MaxCombo;
+ result /= Math.Max(attributes.MaxCombo + 2 * (comboScore - 1), 1);
+
+ return result;
+ }
+
+ ///
+ /// This function is a harsher version of current combo-based miss count, used to provide reasonable value for cases where score-based miss count can't do this.
+ ///
+ private double calculateMaximumComboBasedMissCount()
+ {
+ int countMiss = score.Statistics.GetValueOrDefault(HitResult.Miss);
+
+ if (attributes.SliderCount <= 0)
+ return countMiss;
+
+ int countOk = score.Statistics.GetValueOrDefault(HitResult.Ok);
+ int countMeh = score.Statistics.GetValueOrDefault(HitResult.Meh);
+
+ int totalImperfectHits = countOk + countMeh + countMiss;
+
+ double missCount = 0;
+
+ // Consider that full combo is maximum combo minus dropped slider tails since they don't contribute to combo but also don't break it
+ // In classic scores we can't know the amount of dropped sliders so we estimate to 10% of all sliders on the map
+ double fullComboThreshold = attributes.MaxCombo - 0.1 * attributes.SliderCount;
+
+ if (score.MaxCombo < fullComboThreshold)
+ missCount = Math.Pow(fullComboThreshold / Math.Max(1.0, score.MaxCombo), 2.5);
+
+ // In classic scores there can't be more misses than a sum of all non-perfect judgements
+ missCount = Math.Min(missCount, totalImperfectHits);
+
+ return missCount;
+ }
+
+ ///
+ /// Logic copied from .
+ ///
+ private double getLegacyScoreMultiplier()
+ {
+ bool scoreV2 = score.Mods.Any(m => m is ModScoreV2);
+
+ double multiplier = 1.0;
+
+ foreach (var mod in score.Mods)
+ {
+ switch (mod)
+ {
+ case OsuModNoFail:
+ multiplier *= scoreV2 ? 1.0 : 0.5;
+ break;
+
+ case OsuModEasy:
+ multiplier *= 0.5;
+ break;
+
+ case OsuModHalfTime:
+ case OsuModDaycore:
+ multiplier *= 0.3;
+ break;
+
+ case OsuModHidden:
+ multiplier *= 1.06;
+ break;
+
+ case OsuModHardRock:
+ multiplier *= scoreV2 ? 1.10 : 1.06;
+ break;
+
+ case OsuModDoubleTime:
+ case OsuModNightcore:
+ multiplier *= scoreV2 ? 1.20 : 1.12;
+ break;
+
+ case OsuModFlashlight:
+ multiplier *= 1.12;
+ break;
+
+ case OsuModSpunOut:
+ multiplier *= 0.9;
+ break;
+
+ case OsuModRelax:
+ case OsuModAutopilot:
+ return 0;
+ }
+ }
+
+ return multiplier;
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceAttributes.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceAttributes.cs
index de4491a31b..f889ce3137 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceAttributes.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceAttributes.cs
@@ -27,6 +27,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty
[JsonProperty("speed_deviation")]
public double? SpeedDeviation { get; set; }
+ [JsonProperty("combo_based_estimated_miss_count")]
+ public double ComboBasedEstimatedMissCount { get; set; }
+
+ [JsonProperty("score_based_estimated_miss_count")]
+ public double? ScoreBasedEstimatedMissCount { get; set; }
+
public override IEnumerable GetAttributesForDisplay()
{
foreach (var attribute in base.GetAttributesForDisplay())
diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs
index 431bc24357..1c9334d208 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs
@@ -7,12 +7,12 @@ using System.Linq;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
-using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Difficulty.Utils;
using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Osu.Scoring;
+using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Osu.Difficulty.Skills;
using osu.Game.Rulesets.Osu.Mods;
-using osu.Game.Rulesets.Osu.Scoring;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Utils;
@@ -95,30 +95,20 @@ namespace osu.Game.Rulesets.Osu.Difficulty
overallDifficulty = (80 - greatHitWindow) / 6;
approachRate = preempt > 1200 ? (1800 - preempt) / 120 : (1200 - preempt) / 150 + 5;
- if (osuAttributes.SliderCount > 0)
+ double comboBasedEstimatedMissCount = calculateComboBasedEstimatedMissCount(osuAttributes);
+ double? scoreBasedEstimatedMissCount = null;
+
+ if (usingClassicSliderAccuracy && score.LegacyTotalScore != null)
{
- if (usingClassicSliderAccuracy)
- {
- // Consider that full combo is maximum combo minus dropped slider tails since they don't contribute to combo but also don't break it
- // In classic scores we can't know the amount of dropped sliders so we estimate to 10% of all sliders on the map
- double fullComboThreshold = attributes.MaxCombo - 0.1 * osuAttributes.SliderCount;
+ var legacyScoreMissCalculator = new OsuLegacyScoreMissCalculator(score, osuAttributes);
+ scoreBasedEstimatedMissCount = legacyScoreMissCalculator.Calculate();
- if (scoreMaxCombo < fullComboThreshold)
- effectiveMissCount = fullComboThreshold / Math.Max(1.0, scoreMaxCombo);
-
- // In classic scores there can't be more misses than a sum of all non-perfect judgements
- effectiveMissCount = Math.Min(effectiveMissCount, totalImperfectHits);
- }
- else
- {
- double fullComboThreshold = attributes.MaxCombo - countSliderEndsDropped;
-
- if (scoreMaxCombo < fullComboThreshold)
- effectiveMissCount = fullComboThreshold / Math.Max(1.0, scoreMaxCombo);
-
- // Combine regular misses with tick misses since tick misses break combo as well
- effectiveMissCount = Math.Min(effectiveMissCount, countSliderTickMiss + countMiss);
- }
+ effectiveMissCount = scoreBasedEstimatedMissCount.Value;
+ }
+ else
+ {
+ // Use combo-based miss count if this isn't a legacy score
+ effectiveMissCount = comboBasedEstimatedMissCount;
}
effectiveMissCount = Math.Max(countMiss, effectiveMissCount);
@@ -163,6 +153,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty
Accuracy = accuracyValue,
Flashlight = flashlightValue,
EffectiveMissCount = effectiveMissCount,
+ ComboBasedEstimatedMissCount = comboBasedEstimatedMissCount,
+ ScoreBasedEstimatedMissCount = scoreBasedEstimatedMissCount,
SpeedDeviation = speedDeviation,
Total = totalValue
};
@@ -325,6 +317,39 @@ namespace osu.Game.Rulesets.Osu.Difficulty
return flashlightValue;
}
+ private double calculateComboBasedEstimatedMissCount(OsuDifficultyAttributes attributes)
+ {
+ if (attributes.SliderCount <= 0)
+ return countMiss;
+
+ double missCount = countMiss;
+
+ if (usingClassicSliderAccuracy)
+ {
+ // Consider that full combo is maximum combo minus dropped slider tails since they don't contribute to combo but also don't break it
+ // In classic scores we can't know the amount of dropped sliders so we estimate to 10% of all sliders on the map
+ double fullComboThreshold = attributes.MaxCombo - 0.1 * attributes.SliderCount;
+
+ if (scoreMaxCombo < fullComboThreshold)
+ missCount = fullComboThreshold / Math.Max(1.0, scoreMaxCombo);
+
+ // In classic scores there can't be more misses than a sum of all non-perfect judgements
+ missCount = Math.Min(missCount, totalImperfectHits);
+ }
+ else
+ {
+ double fullComboThreshold = attributes.MaxCombo - countSliderEndsDropped;
+
+ if (scoreMaxCombo < fullComboThreshold)
+ missCount = fullComboThreshold / Math.Max(1.0, scoreMaxCombo);
+
+ // Combine regular misses with tick misses since tick misses break combo as well
+ missCount = Math.Min(missCount, countSliderTickMiss + countMiss);
+ }
+
+ return missCount;
+ }
+
private double calculateEstimatedSliderbreaks(double topWeightedSliderFactor, OsuDifficultyAttributes attributes)
{
if (!usingClassicSliderAccuracy || countOk == 0)
@@ -336,6 +361,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty
// scores with more oks are more likely to have sliderbreaks
double okAdjustment = ((countOk - estimatedSliderbreaks) + 0.5) / countOk;
+ // There is a low probability of extra slider breaks on effective miss counts close to 1, as score based calculations are good at indicating if only a single break occurred.
+ estimatedSliderbreaks *= DifficultyCalculationUtils.Smoothstep(effectiveMissCount, 1, 2);
+
return estimatedSliderbreaks * okAdjustment * DifficultyCalculationUtils.Logistic(missedComboPercent, 0.33, 15);
}
diff --git a/osu.Game.Rulesets.Osu/Difficulty/Utils/LegacyScoreUtils.cs b/osu.Game.Rulesets.Osu/Difficulty/Utils/LegacyScoreUtils.cs
new file mode 100644
index 0000000000..d1df378b47
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Difficulty/Utils/LegacyScoreUtils.cs
@@ -0,0 +1,51 @@
+// 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.Linq;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Objects.Legacy;
+using osu.Game.Rulesets.Osu.Objects;
+
+namespace osu.Game.Rulesets.Osu.Difficulty.Utils
+{
+ public static class LegacyScoreUtils
+ {
+ ///
+ /// Calculates the average amount of score per object that is caused by slider ticks.
+ ///
+ public static double CalculateSliderNestedScorePerObject(IBeatmap beatmap, int objectCount)
+ {
+ const double big_tick_score = 30;
+ const double small_tick_score = 10;
+
+ var sliders = beatmap.HitObjects.OfType().ToArray();
+
+ // 1 for head, 1 for tail
+ int amountOfBigTicks = sliders.Length * 2;
+
+ // Add slider repeats
+ amountOfBigTicks += sliders.Select(s => s.RepeatCount).Sum();
+
+ int amountOfSmallTicks = sliders.Select(s => s.NestedHitObjects.Count(nho => nho is SliderTick)).Sum();
+
+ double totalScore = amountOfBigTicks * big_tick_score + amountOfSmallTicks * small_tick_score;
+
+ return totalScore / objectCount;
+ }
+
+ public static int CalculateDifficultyPeppyStars(IBeatmap beatmap)
+ {
+ int objectCount = beatmap.HitObjects.Count;
+ int drainLength = 0;
+
+ if (objectCount > 0)
+ {
+ int breakLength = beatmap.Breaks.Select(b => (int)Math.Round(b.EndTime) - (int)Math.Round(b.StartTime)).Sum();
+ drainLength = ((int)Math.Round(beatmap.HitObjects[^1].StartTime) - (int)Math.Round(beatmap.HitObjects[0].StartTime) - breakLength) / 1000;
+ }
+
+ return LegacyRulesetExtensions.CalculateDifficultyPeppyStars(beatmap.Difficulty, objectCount, drainLength);
+ }
+ }
+}
diff --git a/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs b/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs
index f2b5642236..e01ce6fde5 100644
--- a/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs
+++ b/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs
@@ -28,6 +28,9 @@ namespace osu.Game.Rulesets.Difficulty
protected const int ATTRIB_ID_AIM_DIFFICULT_SLIDER_COUNT = 31;
protected const int ATTRIB_ID_AIM_TOP_WEIGHTED_SLIDER_FACTOR = 33;
protected const int ATTRIB_ID_SPEED_TOP_WEIGHTED_SLIDER_FACTOR = 35;
+ protected const int ATTRIB_ID_SLIDER_NESTED_SCORE_PER_OBJECT = 37;
+ protected const int ATTRIB_ID_LEGACY_SCORE_BASE_MULTIPLIER = 39;
+ protected const int ATTRIB_ID_MAXIMUM_LEGACY_COMBO_SCORE = 41;
///
/// The mods which were applied to the beatmap.
diff --git a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs
index a7eed0dda1..4a404c1e57 100644
--- a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs
+++ b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs
@@ -28,11 +28,15 @@ namespace osu.Game.Rulesets.Difficulty
///
protected IBeatmap Beatmap { get; private set; }
+ ///
+ /// The working beatmap for which difficulty will be calculated.
+ ///
+ protected readonly IWorkingBeatmap WorkingBeatmap;
+
private Mod[] playableMods;
private double clockRate;
private readonly IRulesetInfo ruleset;
- private readonly IWorkingBeatmap beatmap;
///
/// A yymmdd version which is used to discern when reprocessing is required.
@@ -42,7 +46,7 @@ namespace osu.Game.Rulesets.Difficulty
protected DifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap)
{
this.ruleset = ruleset;
- this.beatmap = beatmap;
+ WorkingBeatmap = beatmap;
}
///
@@ -178,7 +182,7 @@ namespace osu.Game.Rulesets.Difficulty
private void preProcess([NotNull] IEnumerable mods, CancellationToken cancellationToken)
{
playableMods = mods.Select(m => m.DeepClone()).ToArray();
- Beatmap = beatmap.GetPlayableBeatmap(ruleset, playableMods, cancellationToken);
+ Beatmap = WorkingBeatmap.GetPlayableBeatmap(ruleset, playableMods, cancellationToken);
clockRate = ModUtils.CalculateRateWithMods(playableMods);
}