From b8fa87a605d853a5babc4a323a2533993a53ff78 Mon Sep 17 00:00:00 2001
From: finadoggie <75299710+Finadoggie@users.noreply.github.com>
Date: Fri, 8 Nov 2024 17:02:19 -0800
Subject: [PATCH] Deal with accidental merge issues why does merging have to be
so difficult
---
.../Difficulty/OsuPerformanceCalculator.cs | 128 ++++++++++++------
1 file changed, 89 insertions(+), 39 deletions(-)
diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs
index c6395b24e6..e2c9ea77e3 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs
@@ -5,6 +5,7 @@ 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;
@@ -13,7 +14,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty
{
public class OsuPerformanceCalculator : PerformanceCalculator
{
- 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.
+ 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;
private double accuracy;
private int scoreMaxCombo;
@@ -22,6 +25,19 @@ 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()
@@ -33,13 +49,45 @@ 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);
- effectiveMissCount = calculateEffectiveMissCount(osuAttributes);
- accuracy = calculateEffectiveAccuracy(countGreat, countOk, countMeh, countMiss, totalHits);
+ 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);
double multiplier = PERFORMANCE_BASE_MULTIPLIER;
@@ -86,17 +134,14 @@ namespace osu.Game.Rulesets.Osu.Difficulty
private double computeAimValue(ScoreInfo score, OsuDifficultyAttributes attributes)
{
- double aimValue = Math.Pow(5.0 * Math.Max(1.0, attributes.AimDifficulty / 0.0675) - 4.0, 3.0) / 100000.0;
+ double aimValue = OsuStrainSkill.DifficultyToPerformance(attributes.AimDifficulty);
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 *= 0.97 * Math.Pow(1 - Math.Pow(effectiveMissCount / totalHits, 0.775), effectiveMissCount);
-
- aimValue *= getComboScalingFactor(attributes);
+ aimValue *= calculateMissPenalty(effectiveMissCount, attributes.AimDifficultStrainCount);
double approachRateFactor = 0.0;
if (attributes.ApproachRate > 10.33)
@@ -111,7 +156,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(h => h is OsuModHidden))
+ else if (score.Mods.Any(m => m is OsuModHidden || m is OsuModTraceable))
{
// 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);
@@ -122,8 +167,22 @@ namespace osu.Game.Rulesets.Osu.Difficulty
if (attributes.SliderCount > 0)
{
- 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;
+ 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;
aimValue *= sliderNerfFactor;
}
@@ -139,17 +198,14 @@ namespace osu.Game.Rulesets.Osu.Difficulty
if (score.Mods.Any(h => h is OsuModRelax))
return 0.0;
- double speedValue = Math.Pow(5.0 * Math.Max(1.0, attributes.SpeedDifficulty / 0.0675) - 4.0, 3.0) / 100000.0;
+ double speedValue = OsuStrainSkill.DifficultyToPerformance(attributes.SpeedDifficulty);
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 *= 0.97 * Math.Pow(1 - Math.Pow(effectiveMissCount / totalHits, 0.775), Math.Pow(effectiveMissCount, .875));
-
- speedValue *= getComboScalingFactor(attributes);
+ speedValue *= calculateMissPenalty(effectiveMissCount, attributes.SpeedDifficultStrainCount);
double approachRateFactor = 0.0;
if (attributes.ApproachRate > 10.33)
@@ -162,7 +218,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))
+ else if (score.Mods.Any(m => m is OsuModHidden || m is OsuModTraceable))
{
// 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);
@@ -174,10 +230,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 : calculateEffectiveAccuracy(relevantCountGreat, relevantCountOk, relevantCountMeh, relevantCountMiss, attributes.SpeedNoteCount);
+ double relevantAccuracy = attributes.SpeedNoteCount == 0 ? 0 : calculateAccuracy(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 - Math.Max(attributes.OverallDifficulty, 8)) / 2);
+ speedValue *= (0.95 + Math.Pow(attributes.OverallDifficulty, 2) / 750) * Math.Pow((accuracy + relevantAccuracy) / 2.0, (14.5 - attributes.OverallDifficulty) / 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);
@@ -193,9 +249,11 @@ 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 = calculateEffectiveAccuracy(countGreat - (totalHits - amountHitObjectsWithAccuracy), countOk, countMeh, countMiss, amountHitObjectsWithAccuracy);
+ betterAccuracyPercentage = calculateAccuracy(countGreat - (totalHits - amountHitObjectsWithAccuracy), countOk, countMeh, countMiss, amountHitObjectsWithAccuracy);
else
betterAccuracyPercentage = 0;
@@ -213,7 +271,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))
+ else if (score.Mods.Any(m => m is OsuModHidden || m is OsuModTraceable))
accuracyValue *= 1.08;
if (score.Mods.Any(m => m is OsuModFlashlight))
@@ -227,7 +285,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
if (!score.Mods.Any(h => h is OsuModFlashlight))
return 0.0;
- double flashlightValue = Math.Pow(attributes.FlashlightDifficulty, 2.0) * 25.0;
+ double flashlightValue = Flashlight.DifficultyToPerformance(attributes.FlashlightDifficulty);
// Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses.
if (effectiveMissCount > 0)
@@ -247,26 +305,18 @@ namespace osu.Game.Rulesets.Osu.Difficulty
return flashlightValue;
}
- private double calculateEffectiveMissCount(OsuDifficultyAttributes attributes)
- {
- // Guess the number of misses + slider breaks from combo
- double comboBasedMissCount = 0.0;
+ // 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);
- 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 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);
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;
}
}