diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs
index e2c9ea77e3..c6395b24e6 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs
@@ -5,7 +5,6 @@ using System;
using System.Collections.Generic;
using System.Linq;
using osu.Game.Rulesets.Difficulty;
-using osu.Game.Rulesets.Osu.Difficulty.Skills;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
@@ -14,9 +13,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
{
public class OsuPerformanceCalculator : PerformanceCalculator
{
- public const double PERFORMANCE_BASE_MULTIPLIER = 1.15; // This is being adjusted to keep the final pp value scaled around what it used to be when changing things.
-
- private bool usingClassicSliderAccuracy;
+ public const double PERFORMANCE_BASE_MULTIPLIER = 1.14; // This is being adjusted to keep the final pp value scaled around what it used to be when changing things.
private double accuracy;
private int scoreMaxCombo;
@@ -25,19 +22,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty
private int countMeh;
private int countMiss;
- ///
- /// Missed slider ticks that includes missed reverse arrows. Will only be correct on non-classic scores
- ///
- private int countSliderTickMiss;
-
- ///
- /// Amount of missed slider tails that don't break combo. Will only be correct on non-classic scores
- ///
- private int countSliderEndsDropped;
-
- ///
- /// Estimated total amount of combo breaks
- ///
private double effectiveMissCount;
public OsuPerformanceCalculator()
@@ -49,45 +33,13 @@ namespace osu.Game.Rulesets.Osu.Difficulty
{
var osuAttributes = (OsuDifficultyAttributes)attributes;
- usingClassicSliderAccuracy = score.Mods.OfType().Any(m => m.NoSliderHeadAccuracy.Value);
-
scoreMaxCombo = score.MaxCombo;
countGreat = score.Statistics.GetValueOrDefault(HitResult.Great);
countOk = score.Statistics.GetValueOrDefault(HitResult.Ok);
countMeh = score.Statistics.GetValueOrDefault(HitResult.Meh);
countMiss = score.Statistics.GetValueOrDefault(HitResult.Miss);
- countSliderEndsDropped = osuAttributes.SliderCount - score.Statistics.GetValueOrDefault(HitResult.SliderTailHit);
- countSliderTickMiss = score.Statistics.GetValueOrDefault(HitResult.LargeTickMiss);
-
- accuracy = calculateAccuracy(countGreat, countOk, countMeh, countMiss, totalHits);
-
- if (osuAttributes.SliderCount > 0)
- {
- 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;
-
- 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 = Math.Max(countMiss, effectiveMissCount);
+ effectiveMissCount = calculateEffectiveMissCount(osuAttributes);
+ accuracy = calculateEffectiveAccuracy(countGreat, countOk, countMeh, countMiss, totalHits);
double multiplier = PERFORMANCE_BASE_MULTIPLIER;
@@ -134,14 +86,17 @@ namespace osu.Game.Rulesets.Osu.Difficulty
private double computeAimValue(ScoreInfo score, OsuDifficultyAttributes attributes)
{
- double aimValue = OsuStrainSkill.DifficultyToPerformance(attributes.AimDifficulty);
+ double aimValue = Math.Pow(5.0 * Math.Max(1.0, attributes.AimDifficulty / 0.0675) - 4.0, 3.0) / 100000.0;
double lengthBonus = 0.95 + 0.4 * Math.Min(1.0, totalHits / 2000.0) +
(totalHits > 2000 ? Math.Log10(totalHits / 2000.0) * 0.5 : 0.0);
aimValue *= lengthBonus;
+ // Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses.
if (effectiveMissCount > 0)
- aimValue *= calculateMissPenalty(effectiveMissCount, attributes.AimDifficultStrainCount);
+ aimValue *= 0.97 * Math.Pow(1 - Math.Pow(effectiveMissCount / totalHits, 0.775), effectiveMissCount);
+
+ aimValue *= getComboScalingFactor(attributes);
double approachRateFactor = 0.0;
if (attributes.ApproachRate > 10.33)
@@ -156,7 +111,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
if (score.Mods.Any(m => m is OsuModBlinds))
aimValue *= 1.3 + (totalHits * (0.0016 / (1 + 2 * effectiveMissCount)) * Math.Pow(accuracy, 16)) * (1 - 0.003 * attributes.DrainRate * attributes.DrainRate);
- else if (score.Mods.Any(m => m is OsuModHidden || m is OsuModTraceable))
+ else if (score.Mods.Any(h => h is OsuModHidden))
{
// We want to give more reward for lower AR when it comes to aim and HD. This nerfs high AR and buffs lower AR.
aimValue *= 1.0 + 0.04 * (12.0 - attributes.ApproachRate);
@@ -167,22 +122,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty
if (attributes.SliderCount > 0)
{
- double estimateImproperlyFollowedDifficultSliders;
-
- if (usingClassicSliderAccuracy)
- {
- // When the score is considered classic (regardless if it was made on old client or not) we consider all missing combo to be dropped difficult sliders
- int maximumPossibleDroppedSliders = totalImperfectHits;
- estimateImproperlyFollowedDifficultSliders = Math.Clamp(Math.Min(maximumPossibleDroppedSliders, attributes.MaxCombo - scoreMaxCombo), 0, estimateDifficultSliders);
- }
- else
- {
- // We add tick misses here since they too mean that the player didn't follow the slider properly
- // We however aren't adding misses here because missing slider heads has a harsh penalty by itself and doesn't mean that the rest of the slider wasn't followed properly
- estimateImproperlyFollowedDifficultSliders = Math.Min(countSliderEndsDropped + countSliderTickMiss, estimateDifficultSliders);
- }
-
- double sliderNerfFactor = (1 - attributes.SliderFactor) * Math.Pow(1 - estimateImproperlyFollowedDifficultSliders / estimateDifficultSliders, 3) + attributes.SliderFactor;
+ double estimateSliderEndsDropped = Math.Clamp(Math.Min(countOk + countMeh + countMiss, attributes.MaxCombo - scoreMaxCombo), 0, estimateDifficultSliders);
+ double sliderNerfFactor = (1 - attributes.SliderFactor) * Math.Pow(1 - estimateSliderEndsDropped / estimateDifficultSliders, 3) + attributes.SliderFactor;
aimValue *= sliderNerfFactor;
}
@@ -198,14 +139,17 @@ namespace osu.Game.Rulesets.Osu.Difficulty
if (score.Mods.Any(h => h is OsuModRelax))
return 0.0;
- double speedValue = OsuStrainSkill.DifficultyToPerformance(attributes.SpeedDifficulty);
+ double speedValue = Math.Pow(5.0 * Math.Max(1.0, attributes.SpeedDifficulty / 0.0675) - 4.0, 3.0) / 100000.0;
double lengthBonus = 0.95 + 0.4 * Math.Min(1.0, totalHits / 2000.0) +
(totalHits > 2000 ? Math.Log10(totalHits / 2000.0) * 0.5 : 0.0);
speedValue *= lengthBonus;
+ // Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses.
if (effectiveMissCount > 0)
- speedValue *= calculateMissPenalty(effectiveMissCount, attributes.SpeedDifficultStrainCount);
+ speedValue *= 0.97 * Math.Pow(1 - Math.Pow(effectiveMissCount / totalHits, 0.775), Math.Pow(effectiveMissCount, .875));
+
+ speedValue *= getComboScalingFactor(attributes);
double approachRateFactor = 0.0;
if (attributes.ApproachRate > 10.33)
@@ -218,7 +162,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
// Increasing the speed value by object count for Blinds isn't ideal, so the minimum buff is given.
speedValue *= 1.12;
}
- else if (score.Mods.Any(m => m is OsuModHidden || m is OsuModTraceable))
+ else if (score.Mods.Any(m => m is OsuModHidden))
{
// We want to give more reward for lower AR when it comes to aim and HD. This nerfs high AR and buffs lower AR.
speedValue *= 1.0 + 0.04 * (12.0 - attributes.ApproachRate);
@@ -230,10 +174,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty
double relevantCountOk = Math.Max(0, countOk - Math.Max(0, relevantTotalDiff - countGreat));
double relevantCountMeh = Math.Max(0, countMeh - Math.Max(0, relevantTotalDiff - countGreat - countOk));
double relevantCountMiss = Math.Max(0, countMiss - Math.Max(0, relevantTotalDiff - countGreat - countOk - countMeh));
- double relevantAccuracy = attributes.SpeedNoteCount == 0 ? 0 : calculateAccuracy(relevantCountGreat, relevantCountOk, relevantCountMeh, relevantCountMiss, attributes.SpeedNoteCount);
+ double relevantAccuracy = attributes.SpeedNoteCount == 0 ? 0 : calculateEffectiveAccuracy(relevantCountGreat, relevantCountOk, relevantCountMeh, relevantCountMiss, attributes.SpeedNoteCount);
// Scale the speed value with accuracy and OD.
- speedValue *= (0.95 + Math.Pow(attributes.OverallDifficulty, 2) / 750) * Math.Pow((accuracy + relevantAccuracy) / 2.0, (14.5 - attributes.OverallDifficulty) / 2);
+ speedValue *= (0.95 + Math.Pow(attributes.OverallDifficulty, 2) / 750) * Math.Pow((accuracy + relevantAccuracy) / 2.0, (14.5 - Math.Max(attributes.OverallDifficulty, 8)) / 2);
// Scale the speed value with # of 50s to punish doubletapping.
speedValue *= Math.Pow(0.99, countMeh < totalHits / 500.0 ? 0 : countMeh - totalHits / 500.0);
@@ -249,11 +193,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty
// This percentage only considers HitCircles of any value - in this part of the calculation we focus on hitting the timing hit window.
double betterAccuracyPercentage;
int amountHitObjectsWithAccuracy = attributes.HitCircleCount;
- if (!usingClassicSliderAccuracy)
- amountHitObjectsWithAccuracy += attributes.SliderCount;
if (amountHitObjectsWithAccuracy > 0)
- betterAccuracyPercentage = calculateAccuracy(countGreat - (totalHits - amountHitObjectsWithAccuracy), countOk, countMeh, countMiss, amountHitObjectsWithAccuracy);
+ betterAccuracyPercentage = calculateEffectiveAccuracy(countGreat - (totalHits - amountHitObjectsWithAccuracy), countOk, countMeh, countMiss, amountHitObjectsWithAccuracy);
else
betterAccuracyPercentage = 0;
@@ -271,7 +213,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
// Increasing the accuracy value by object count for Blinds isn't ideal, so the minimum buff is given.
if (score.Mods.Any(m => m is OsuModBlinds))
accuracyValue *= 1.14;
- else if (score.Mods.Any(m => m is OsuModHidden || m is OsuModTraceable))
+ else if (score.Mods.Any(m => m is OsuModHidden))
accuracyValue *= 1.08;
if (score.Mods.Any(m => m is OsuModFlashlight))
@@ -285,7 +227,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
if (!score.Mods.Any(h => h is OsuModFlashlight))
return 0.0;
- double flashlightValue = Flashlight.DifficultyToPerformance(attributes.FlashlightDifficulty);
+ double flashlightValue = Math.Pow(attributes.FlashlightDifficulty, 2.0) * 25.0;
// Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses.
if (effectiveMissCount > 0)
@@ -305,18 +247,26 @@ namespace osu.Game.Rulesets.Osu.Difficulty
return flashlightValue;
}
- // Miss penalty assumes that a player will miss on the hardest parts of a map,
- // so we use the amount of relatively difficult sections to adjust miss penalty
- // to make it more punishing on maps with lower amount of hard sections.
- private double calculateMissPenalty(double missCount, double difficultStrainCount) => 0.96 / ((missCount / (4 * Math.Pow(Math.Log(difficultStrainCount), 0.94))) + 1);
+ private double calculateEffectiveMissCount(OsuDifficultyAttributes attributes)
+ {
+ // Guess the number of misses + slider breaks from combo
+ double comboBasedMissCount = 0.0;
- private double calculateAccuracy(double countGreat, double countOk, double countMeh, double countMiss, double totalHits)
- => usingClassicSliderAccuracy
- ? (countGreat * 6 + countOk * 2 + countMeh + countMiss * 2) / (totalHits * 6)
- : (countGreat * 6 + countOk * 2 + countMeh) / ((totalHits - countMiss) * 6);
+ if (attributes.SliderCount > 0)
+ {
+ double fullComboThreshold = attributes.MaxCombo - 0.1 * attributes.SliderCount;
+ if (scoreMaxCombo < fullComboThreshold)
+ comboBasedMissCount = fullComboThreshold / Math.Max(1.0, scoreMaxCombo);
+ }
+
+ // Clamp miss count to maximum amount of possible breaks
+ comboBasedMissCount = Math.Min(comboBasedMissCount, countOk + countMeh + countMiss);
+
+ return Math.Max(countMiss, comboBasedMissCount);
+ }
+ private double calculateEffectiveAccuracy(double countGreat, double countOk, double countMeh, double countMiss, double totalHits) => (countGreat * 6 + countOk * 2 + countMeh + countMiss * 2) / (double)(totalHits * 6);
private double getComboScalingFactor(OsuDifficultyAttributes attributes) => attributes.MaxCombo <= 0 ? 1.0 : Math.Min(Math.Pow(scoreMaxCombo, 0.8) / Math.Pow(attributes.MaxCombo, 0.8), 1.0);
private int totalHits => countGreat + countOk + countMeh + countMiss;
- private int totalImperfectHits => countOk + countMeh + countMiss;
}
}