1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-18 11:02:57 +08:00

Merge like half of givi's changes to compare

This commit is contained in:
02Naitsirk 2024-08-29 20:55:52 -04:00
parent aeb7ecf07d
commit c64ad60cb1
2 changed files with 145 additions and 92 deletions

View File

@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
private double effectiveMissCount;
private double hitWindow300;
private double hitWindow300, hitWindow100, hitWindow50;
private double deviation, speedDeviation;
public OsuPerformanceCalculator()
@ -70,8 +70,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty
double clockRate = getClockRate(score);
hitWindow300 = 80 - 6 * osuAttributes.OverallDifficulty;
hitWindow100 = (140 - 8 * ((80 - hitWindow300 * clockRate) / 6)) / clockRate;
hitWindow50 = (200 - 10 * ((80 - hitWindow300 * clockRate) / 6)) / clockRate;
deviation = calculateDeviation(score, osuAttributes);
deviation = calculateTotalDeviation(score, osuAttributes);
speedDeviation = calculateSpeedDeviation(score, osuAttributes);
double aimValue = computeAimValue(score, osuAttributes);
@ -146,9 +148,14 @@ namespace osu.Game.Rulesets.Osu.Difficulty
aimValue *= sliderNerfFactor;
}
double prod = Math.Sqrt(attributes.AimDifficulty * attributes.SpeedDifficulty) * deviation;
aimValue *= Math.Pow(SpecialFunctions.Erf(130 / prod), 1.5);
aimValue *= 0.98 + Math.Pow(100.0 / 9, 2) / 2500; // OD 11.1 SS stays the same.
// Apply antirake nerf
double totalAntiRakeMultiplier = calculateTotalRakeNerf(attributes);
aimValue *= totalAntiRakeMultiplier;
// Scale the aim value with adjusted deviation
double adjustedDeviation = deviation * calculateDeviationArAdjust(attributes.ApproachRate);
aimValue *= SpecialFunctions.Erf(33 / (Math.Sqrt(2) * adjustedDeviation));
aimValue *= 0.98 + Math.Pow(100.0 / 9, 2) / 2500; // OD 11 SS stays the same.
return aimValue;
}
@ -187,9 +194,13 @@ namespace osu.Game.Rulesets.Osu.Difficulty
speedValue *= 1.0 + 0.04 * (12.0 - attributes.ApproachRate);
}
double prod = attributes.SpeedDifficulty * speedDeviation;
speedValue *= Math.Pow(SpecialFunctions.Erf(67.5 / prod), 1.5);
speedValue *= 0.95 + Math.Pow(100.0 / 9, 2) / 750; // OD 11.1 SS stays the same.
// Apply antirake nerf
double speedAntiRakeMultiplier = calculateSpeedRakeNerf(attributes);
speedValue *= speedAntiRakeMultiplier;
double adjustedSpeedDeviation = speedDeviation * calculateDeviationArAdjust(attributes.ApproachRate);
speedValue *= SpecialFunctions.Erf(22 / (Math.Sqrt(2) * adjustedSpeedDeviation * Math.Max(1, Math.Pow(attributes.SpeedDifficulty / 4.5, 1.2))));
speedValue *= 0.95 + Math.Pow(100.0 / 9, 2) / 750; // OD 11 SS stays the same.
return speedValue;
}
@ -230,8 +241,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty
flashlightValue *= 0.7 + 0.1 * Math.Min(1.0, totalHits / 200.0) +
(totalHits > 200 ? 0.2 * Math.Min(1.0, (totalHits - 200) / 200.0) : 0.0);
// Scale the flashlight value with deviation
flashlightValue *= SpecialFunctions.Erf(35 / deviation);
// Scale the flashlight value with adjusted deviation
double adjustedDeviation = deviation * calculateDeviationArAdjust(attributes.ApproachRate);
flashlightValue *= SpecialFunctions.Erf(55 / (Math.Sqrt(2) * adjustedDeviation));
flashlightValue *= 0.98 + Math.Pow(100.0 / 9, 2) / 2500; // OD 11 SS stays the same.
return flashlightValue;
@ -255,6 +267,40 @@ namespace osu.Game.Rulesets.Osu.Difficulty
return Math.Max(countMiss, comboBasedMissCount);
}
/// <summary>
/// Using <see cref="calculateDeviation"/> estimates player's deviation on accuracy objects.
/// Returns deviation for circles and sliders if score was set with slideracc.
/// Returns the min between deviation of circles and deviation on circles and sliders (assuming slider hits are 50s), if score was set without slideracc.
/// </summary>
private double calculateTotalDeviation(ScoreInfo score, OsuDifficultyAttributes attributes)
{
if (totalSuccessfulHits == 0)
return double.PositiveInfinity;
int accuracyObjectCount = attributes.HitCircleCount;
// Assume worst case: all mistakes was on accuracy objects
int relevantCountMiss = Math.Min(countMiss, accuracyObjectCount);
int relevantCountMeh = Math.Min(countMeh, accuracyObjectCount - relevantCountMiss);
int relevantCountOk = Math.Min(countOk, accuracyObjectCount - relevantCountMiss - relevantCountMeh);
int relevantCountGreat = Math.Max(0, accuracyObjectCount - relevantCountMiss - relevantCountMeh - relevantCountOk);
// Calculate deviation on accuracy objects
double deviation = calculateDeviation(relevantCountGreat, relevantCountOk, relevantCountMeh, relevantCountMiss);
// If score was set without slider accuracy - also compute deviation with sliders
// Assume that all hits was 50s
int totalCountWithSliders = attributes.HitCircleCount + attributes.SliderCount;
int missCountWithSliders = Math.Min(totalCountWithSliders, countMiss);
int hitCountWithSliders = totalCountWithSliders - missCountWithSliders;
double hitProbabilityWithSliders = hitCountWithSliders / (totalCountWithSliders + 1.0);
double deviationWithSliders = hitWindow50 / (Math.Sqrt(2) * SpecialFunctions.ErfInv(hitProbabilityWithSliders));
// Min is needed for edgecase maps with 1 circle and 999 sliders, as deviation on sliders can be lower in this case
return Math.Min(deviation, deviationWithSliders);
}
/// <summary>
/// Using <see cref="calculateDeviation"/> estimates player's deviation on speed notes, assuming worst-case.
/// Treats all speed notes as hit circles. This is not good way to do this, but fixing this is impossible under the limitation of current speed pp.
@ -265,49 +311,27 @@ namespace osu.Game.Rulesets.Osu.Difficulty
/// Treats all difficult speed notes as circles, so this method can sometimes return a lower deviation than <see cref="calculateDeviation"/>.
/// This is fine though, since this method is only used to scale speed pp.
/// </summary>
/// <summary>
/// Using <see cref="calculateDeviation"/> estimates player's deviation on speed notes, assuming worst-case.
/// Treats all speed notes as hit circles. This is not good way to do this, but fixing this is impossible under the limitation of current speed pp.
/// If score was set with slideracc - tries to remove mistaps on sliders from total mistaps.
/// </summary>
private double calculateSpeedDeviation(ScoreInfo score, OsuDifficultyAttributes attributes)
{
if (totalSuccessfulHits == 0)
return double.PositiveInfinity;
// Create a new track to properly calculate the hit windows of 100s and 50s.
double clockRate = getClockRate(score);
double hitWindow300 = 80 - 6 * attributes.OverallDifficulty;
double hitWindow100 = (140 - 8 * ((80 - hitWindow300 * clockRate) / 6)) / clockRate;
double hitWindow50 = (200 - 10 * ((80 - hitWindow300 * clockRate) / 6)) / clockRate;
// Calculate accuracy assuming the worst case scenario
double speedNoteCount = attributes.SpeedNoteCount;
double relevantTotalDiff = totalHits - attributes.SpeedNoteCount;
double relevantCountGreat = Math.Max(0, countGreat - relevantTotalDiff);
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));
// Assume 100s, 50s, and misses happen on circles. If there are less non-300s on circles than 300s,
// compute the deviation on circles.
if (relevantCountGreat > 0)
{
// The probability that a player hits a circle is unknown, but we can estimate it to be
// the number of greats on circles divided by the number of circles, and then add one
// to the number of circles as a bias correction.
double greatProbabilityCircle = relevantCountGreat / (speedNoteCount - relevantCountMiss - relevantCountMeh + 1.0);
// Assume worst case: all mistakes was on speed notes
double relevantCountMiss = Math.Min(countMiss, speedNoteCount);
double relevantCountMeh = Math.Min(countMeh, speedNoteCount - relevantCountMiss);
double relevantCountOk = Math.Min(countOk, speedNoteCount - relevantCountMiss - relevantCountMeh);
double relevantCountGreat = Math.Max(0, speedNoteCount - relevantCountMiss - relevantCountMeh - relevantCountOk);
// Compute the deviation assuming 300s and 100s are normally distributed, and 50s are uniformly distributed.
// Begin with the normal distribution first.
double deviationOnCircles = hitWindow300 / (Math.Sqrt(2) * SpecialFunctions.ErfInv(greatProbabilityCircle));
// Then compute the variance for 50s.
double mehVariance = (hitWindow50 * hitWindow50 + hitWindow100 * hitWindow50 + hitWindow100 * hitWindow100) / 3;
// Find the total deviation.
deviationOnCircles = Math.Sqrt(((relevantCountGreat + relevantCountOk) * Math.Pow(deviationOnCircles, 2) + relevantCountMeh * mehVariance) / (relevantCountGreat + relevantCountOk + relevantCountMeh));
return deviationOnCircles;
}
return double.PositiveInfinity;
// Calculate and return deviation on speed notes
return calculateDeviation(relevantCountGreat, relevantCountOk, relevantCountMeh, relevantCountMiss);
}
/// <summary>
@ -316,69 +340,98 @@ namespace osu.Game.Rulesets.Osu.Difficulty
/// will always return the same deviation. Misses are ignored because they are usually due to misaiming.
/// 300s and 100s are assumed to follow a normal distribution, whereas 50s are assumed to follow a uniform distribution.
/// </summary>
private double calculateDeviation(ScoreInfo score, OsuDifficultyAttributes attributes)
private double calculateDeviation(double relevantCountGreat, double relevantCountOk, double relevantCountMeh, double relevantCountMiss)
{
if (totalSuccessfulHits == 0)
if (relevantCountGreat + relevantCountOk + relevantCountMeh <= 0)
return double.PositiveInfinity;
double clockRate = getClockRate(score);
double objectCount = relevantCountGreat + relevantCountOk + relevantCountMeh + relevantCountMiss;
double hitWindow300 = 80 - 6 * attributes.OverallDifficulty;
double hitWindow100 = (140 - 8 * ((80 - hitWindow300 * clockRate) / 6)) / clockRate;
double hitWindow50 = (200 - 10 * ((80 - hitWindow300 * clockRate) / 6)) / clockRate;
//// The probability that a player hits a circle is unknown, but we can estimate it to be
//// the number of greats on circles divided by the number of circles, and then add one
//// to the number of circles as a bias correction.
double n = Math.Max(1, objectCount - relevantCountMiss - relevantCountMeh);
const double z = 2.32634787404; // 99% critical value for the normal distribution (one-tailed).
int circleCount = attributes.HitCircleCount;
int missCountCircles = Math.Min(countMiss, circleCount);
int mehCountCircles = Math.Min(countMeh, circleCount - missCountCircles);
int okCountCircles = Math.Min(countOk, circleCount - missCountCircles - mehCountCircles);
int greatCountCircles = Math.Max(0, circleCount - missCountCircles - mehCountCircles - okCountCircles);
// Proportion of greats hit on circles, ignoring misses and 50s.
double p = relevantCountGreat / n;
// Assume 100s, 50s, and misses happen on circles. If there are less non-300s on circles than 300s,
// compute the deviation on circles.
if (greatCountCircles > 0)
{
double n = circleCount - missCountCircles - mehCountCircles;
const double z = 2.32634787404; // 99% critical value for the normal distribution (one-tailed).
// We can be 99% confident that p is at least this value.
double pLowerBound = (n * p + z * z / 2) / (n + z * z) - z / (n + z * z) * Math.Sqrt(n * p * (1 - p) + z * z / 4);
// Proportion of greats hit on circles, ignoring misses and 50s.
double p = greatCountCircles / n;
// Compute the deviation assuming 300s and 100s are normally distributed, and 50s are uniformly distributed.
// Begin with 300s and 100s first. Ignoring 50s, we can be 99% confident that the deviation is not higher than:
double deviation = hitWindow300 / (Math.Sqrt(2) * SpecialFunctions.ErfInv(pLowerBound));
// We can be 99% confident that p is at least this value.
double pLowerBound = (n * p + z * z / 2) / (n + z * z) - z / (n + z * z) * Math.Sqrt(n * p * (1 - p) + z * z / 4);
double adjustFor100 = Math.Sqrt(2 / Math.PI) * hitWindow100 * Math.Exp(-0.5 * Math.Pow(hitWindow100 / deviation, 2))
/ (deviation * SpecialFunctions.Erf(hitWindow100 / (Math.Sqrt(2) * deviation)));
// Compute the deviation assuming 300s and 100s are normally distributed, and 50s are uniformly distributed.
// Begin with 300s and 100s first. Ignoring 50s, we can be 99% confident that the deviation is not higher than:
double deviationOnCircles = hitWindow300 / (Math.Sqrt(2) * SpecialFunctions.ErfInv(pLowerBound));
deviation *= Math.Sqrt(1 - adjustFor100);
// Then compute the variance for 50s.
double mehVariance = (hitWindow50 * hitWindow50 + hitWindow100 * hitWindow50 + hitWindow100 * hitWindow100) / 3;
// Value deviation approach as greatCount approaches 0
double limitValue = hitWindow100 / Math.Sqrt(3);
// Find the total deviation.
deviationOnCircles = Math.Sqrt(((greatCountCircles + okCountCircles) * Math.Pow(deviationOnCircles, 2) + mehCountCircles * mehVariance) / (greatCountCircles + okCountCircles + mehCountCircles));
// If precision is not enough to compute true deviation - use limit value
if (pLowerBound == 0 || adjustFor100 >= 1 || deviation > limitValue)
deviation = limitValue;
return deviationOnCircles;
}
// Then compute the variance for 50s.
double mehVariance = (hitWindow50 * hitWindow50 + hitWindow100 * hitWindow50 + hitWindow100 * hitWindow100) / 3;
// If there are more non-300s than there are circles, compute the deviation on sliders instead.
// Here, all that matters is whether or not the slider was missed, since it is impossible
// to get a 100 or 50 on a slider by mis-tapping it.
int sliderCount = attributes.SliderCount;
int missCountSliders = Math.Min(sliderCount, countMiss - missCountCircles);
int greatCountSliders = sliderCount - missCountSliders;
// Find the total deviation.
deviation = Math.Sqrt(((relevantCountGreat + relevantCountOk) * Math.Pow(deviation, 2) + relevantCountMeh * mehVariance) / (relevantCountGreat + relevantCountOk + relevantCountMeh));
// We only get here if nothing was hit. In this case, there is no estimate for deviation.
// Note that this is never negative, so checking if this is only equal to 0 makes sense.
if (greatCountSliders == 0)
{
return double.PositiveInfinity;
}
double greatProbabilitySlider = greatCountSliders / (sliderCount + 1.0);
double deviationOnSliders = hitWindow50 / (Math.Sqrt(2) * SpecialFunctions.ErfInv(greatProbabilitySlider));
return deviationOnSliders;
return deviation;
}
// Calculates multiplier for speed accounting for rake based on the deviation and speed difficulty
// https://www.desmos.com/calculator/puc1mzdtfv
private double calculateSpeedRakeNerf(OsuDifficultyAttributes attributes)
{
// Base speed value
double speedValue = 4 * Math.Pow(attributes.SpeedDifficulty, 3);
// Starting from this pp amount - penalty will be applied
double abusePoint = 100 + 260 * Math.Pow(22 / speedDeviation, 5.8);
if (speedValue <= abusePoint)
return 1.0;
// Use log curve to make additional rise in difficulty unimpactful. Rescale values to make curve have correct steepness
const double scale = 50;
double adjustedSpeedValue = scale * (Math.Log((speedValue - abusePoint) / scale + 1) + abusePoint / scale);
double speedRakeNerf = adjustedSpeedValue / speedValue;
return Math.Min(speedRakeNerf, calculateTotalRakeNerf(attributes));
}
// Calculates multiplier for total pp accounting for rake based on the deviation and sliderless aim and speed difficulty
private double calculateTotalRakeNerf(OsuDifficultyAttributes attributes)
{
// Use adjusted deviation to not nerf EZHT aim maps
double adjustedDeviation = deviation * calculateDeviationArAdjust(attributes.ApproachRate);
// Base values
double aimNoSlidersValue = 4 * Math.Pow(attributes.AimDifficulty * attributes.SliderFactor, 3);
double speedValue = 4 * Math.Pow(attributes.SpeedDifficulty, 3);
double totalValue = Math.Pow(Math.Pow(aimNoSlidersValue, 1.1) + Math.Pow(speedValue, 1.1), 1 / 1.1);
// Starting from this pp amount - penalty will be applied
double abusePoint = 200 + 600 * Math.Pow(22 / adjustedDeviation, 4.2);
if (totalValue <= abusePoint)
return 1.0;
// Use relax penalty after the point to make values grow slower but still noticeably
double adjustedTotalValue = abusePoint + Math.Pow(0.9, 3) * (totalValue - abusePoint);
return adjustedTotalValue / totalValue;
}
// Bonus for low AR to account for the fact that it's more difficult to get low UR on low AR
private static double calculateDeviationArAdjust(double AR) => 0.475 + 0.7 / (1.0 + Math.Pow(1.73, 7.9 - AR));
private static double getClockRate(ScoreInfo score)
{
var track = new TrackVirtual(1);

View File

@ -59,7 +59,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
if (maxStrain == 0)
return 0;
return objectStrains.Sum(strain => strain / maxStrain);
return objectStrains.Sum(strain => 1.0 / (1.0 + Math.Exp(-(strain / maxStrain * 8.0 - 4.0))));
}
}
}