1
0
mirror of https://github.com/ppy/osu.git synced 2026-05-26 11:30:04 +08:00

Move rating calculations to OsuRatingCalculator (#33265)

* Move rating calculations to `OsuRatingCalculator`

* Use `CalculateDifficultyRating`
This commit is contained in:
James Wilson
2025-07-25 16:47:21 +01:00
committed by GitHub
Unverified
parent 945db7b431
commit 28d36dd3bd
3 changed files with 216 additions and 187 deletions
@@ -8,7 +8,6 @@ using osu.Game.Beatmaps;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Difficulty.Skills;
using osu.Game.Rulesets.Difficulty.Utils;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
using osu.Game.Rulesets.Osu.Difficulty.Skills;
@@ -23,13 +22,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty
public class OsuDifficultyCalculator : DifficultyCalculator
{
private 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 const double difficulty_multiplier = 0.0675;
private const double star_rating_multiplier = 0.0265;
public override int Version => 20250306;
private double mechanicalDifficultyRating;
public OsuDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap)
: base(ruleset, beatmap)
{
@@ -45,30 +41,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty
return multiplier;
}
/// <summary>
/// Calculates a visibility bonus that is applicable to Hidden and Traceable.
/// </summary>
public static double CalculateVisibilityBonus(Mod[] mods, double approachRate, double visibilityFactor = 1)
{
// NOTE: TC's effect is only noticeable in performance calculations until lazer mods are accounted for server-side.
bool isAlwaysPartiallyVisible = mods.OfType<OsuModHidden>().Any(m => m.OnlyFadeApproachCircles.Value) || mods.OfType<OsuModTraceable>().Any();
// Start from normal curve, rewarding lower AR up to AR5
double readingBonus = 0.04 * (12.0 - Math.Max(approachRate, 5));
readingBonus *= visibilityFactor;
// For AR up to 0 - reduce reward for very low ARs when object is visible
if (approachRate < 5)
readingBonus += (isAlwaysPartiallyVisible ? 0.03 : 0.04) * (5.0 - Math.Max(approachRate, 0));
// Starting from AR0 - cap values so they won't grow to infinity
if (approachRate < 0)
readingBonus += (isAlwaysPartiallyVisible ? 0.075 : 0.1) * (1 - Math.Pow(1.5, approachRate));
return readingBonus;
}
public static double CalculateRateAdjustedApproachRate(double approachRate, double clockRate)
{
double preempt = IBeatmapDifficultyInfo.DifficultyRange(approachRate, OsuHitObject.PREEMPT_MAX, OsuHitObject.PREEMPT_MID, OsuHitObject.PREEMPT_MIN) / clockRate;
@@ -125,19 +97,27 @@ namespace osu.Game.Rulesets.Osu.Difficulty
double aimNoSlidersDifficultyValue = aimWithoutSliders.DifficultyValue();
double speedDifficultyValue = speed.DifficultyValue();
mechanicalDifficultyRating = calculateMechanicalDifficultyRating(aimDifficultyValue, speedDifficultyValue);
double mechanicalDifficultyRating = calculateMechanicalDifficultyRating(aimDifficultyValue, speedDifficultyValue);
double aimRating = computeAimRating(aimDifficultyValue, mods, totalHits, approachRate, overallDifficulty);
double aimRatingNoSliders = computeAimRating(aimNoSlidersDifficultyValue, mods, totalHits, approachRate, overallDifficulty);
double speedRating = computeSpeedRating(speedDifficultyValue, mods, totalHits, approachRate, overallDifficulty);
var osuRatingCalculator = new OsuRatingCalculator(mods, totalHits, approachRate, overallDifficulty, mechanicalDifficultyRating);
double aimRating = osuRatingCalculator.ComputeAimRating(aimDifficultyValue);
double aimRatingNoSliders = osuRatingCalculator.ComputeAimRating(aimNoSlidersDifficultyValue);
double speedRating = osuRatingCalculator.ComputeSpeedRating(speedDifficultyValue);
double flashlightRating = 0.0;
if (flashlight is not null)
flashlightRating = computeFlashlightRating(flashlight.DifficultyValue(), mods, totalHits, overallDifficulty);
flashlightRating = osuRatingCalculator.ComputeFlashlightRating(flashlight.DifficultyValue());
double sliderFactor = aimRating > 0 ? aimRatingNoSliders / aimRating : 1;
double sliderNestedScorePerObject = LegacyScoreUtils.CalculateNestedScorePerObject(beatmap, totalHits);
double legacyScoreBaseMultiplier = LegacyScoreUtils.CalculateDifficultyPeppyStars(beatmap);
var simulator = new OsuLegacyScoreSimulator();
var scoreAttributes = simulator.Simulate(WorkingBeatmap, beatmap);
double baseAimPerformance = OsuStrainSkill.DifficultyToPerformance(aimRating);
double baseSpeedPerformance = OsuStrainSkill.DifficultyToPerformance(speedRating);
double baseFlashlightPerformance = Flashlight.DifficultyToPerformance(flashlightRating);
@@ -152,12 +132,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty
double multiplier = CalculateDifficultyMultiplier(mods, totalHits, spinnerCount);
double starRating = calculateStarRating(basePerformance, multiplier);
double sliderNestedScorePerObject = LegacyScoreUtils.CalculateNestedScorePerObject(beatmap, totalHits);
double legacyScoreBaseMultiplier = LegacyScoreUtils.CalculateDifficultyPeppyStars(beatmap);
var simulator = new OsuLegacyScoreSimulator();
var scoreAttributes = simulator.Simulate(WorkingBeatmap, beatmap);
OsuDifficultyAttributes attributes = new OsuDifficultyAttributes
{
StarRating = starRating,
@@ -185,152 +159,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty
return attributes;
}
private double computeAimRating(double aimDifficultyValue, Mod[] mods, int totalHits, double approachRate, double overallDifficulty)
{
if (mods.Any(m => m is OsuModAutopilot))
return 0;
double aimRating = calculateDifficultyRating(aimDifficultyValue);
if (mods.Any(m => m is OsuModTouchDevice))
aimRating = Math.Pow(aimRating, 0.8);
if (mods.Any(m => m is OsuModRelax))
aimRating *= 0.9;
if (mods.Any(m => m is OsuModMagnetised))
{
float magnetisedStrength = mods.OfType<OsuModMagnetised>().First().AttractionStrength.Value;
aimRating *= 1.0 - magnetisedStrength;
}
double ratingMultiplier = 1.0;
double approachRateLengthBonus = 0.95 + 0.4 * Math.Min(1.0, totalHits / 2000.0) +
(totalHits > 2000 ? Math.Log10(totalHits / 2000.0) * 0.5 : 0.0);
double approachRateFactor = 0.0;
if (approachRate > 10.33)
approachRateFactor = 0.3 * (approachRate - 10.33);
else if (approachRate < 8.0)
approachRateFactor = 0.05 * (8.0 - approachRate);
if (mods.Any(h => h is OsuModRelax))
approachRateFactor = 0.0;
ratingMultiplier *= 1.0 + approachRateFactor * approachRateLengthBonus; // Buff for longer maps with high AR.
if (mods.Any(m => m is OsuModHidden))
{
double visibilityFactor = calculateAimVisibilityFactor(approachRate);
ratingMultiplier *= 1.0 + CalculateVisibilityBonus(mods, approachRate, visibilityFactor);
}
// It is important to consider accuracy difficulty when scaling with accuracy.
ratingMultiplier *= 0.98 + Math.Pow(Math.Max(0, overallDifficulty), 2) / 2500;
return aimRating * Math.Cbrt(ratingMultiplier);
}
private double computeSpeedRating(double speedDifficultyValue, Mod[] mods, int totalHits, double approachRate, double overallDifficulty)
{
if (mods.Any(m => m is OsuModRelax))
return 0;
double speedRating = calculateDifficultyRating(speedDifficultyValue);
if (mods.Any(m => m is OsuModAutopilot))
speedRating *= 0.5;
if (mods.Any(m => m is OsuModMagnetised))
{
// reduce speed rating because of the speed distance scaling, with maximum reduction being 0.7x
float magnetisedStrength = mods.OfType<OsuModMagnetised>().First().AttractionStrength.Value;
speedRating *= 1.0 - magnetisedStrength * 0.3;
}
double ratingMultiplier = 1.0;
double approachRateLengthBonus = 0.95 + 0.4 * Math.Min(1.0, totalHits / 2000.0) +
(totalHits > 2000 ? Math.Log10(totalHits / 2000.0) * 0.5 : 0.0);
double approachRateFactor = 0.0;
if (approachRate > 10.33)
approachRateFactor = 0.3 * (approachRate - 10.33);
if (mods.Any(m => m is OsuModAutopilot))
approachRateFactor = 0.0;
ratingMultiplier *= 1.0 + approachRateFactor * approachRateLengthBonus; // Buff for longer maps with high AR.
if (mods.Any(m => m is OsuModHidden))
{
double visibilityFactor = calculateSpeedVisibilityFactor(approachRate);
ratingMultiplier *= 1.0 + CalculateVisibilityBonus(mods, approachRate, visibilityFactor);
}
ratingMultiplier *= 0.95 + Math.Pow(Math.Max(0, overallDifficulty), 2) / 750;
return speedRating * Math.Cbrt(ratingMultiplier);
}
private double computeFlashlightRating(double flashlightDifficultyValue, Mod[] mods, int totalHits, double overallDifficulty)
{
if (!mods.Any(m => m is OsuModFlashlight))
return 0;
double flashlightRating = calculateDifficultyRating(flashlightDifficultyValue);
if (mods.Any(m => m is OsuModTouchDevice))
flashlightRating = Math.Pow(flashlightRating, 0.8);
if (mods.Any(m => m is OsuModRelax))
flashlightRating *= 0.7;
else if (mods.Any(m => m is OsuModAutopilot))
flashlightRating *= 0.4;
if (mods.Any(m => m is OsuModMagnetised))
{
float magnetisedStrength = mods.OfType<OsuModMagnetised>().First().AttractionStrength.Value;
flashlightRating *= 1.0 - magnetisedStrength;
}
double ratingMultiplier = 1.0;
// Account for shorter maps having a higher ratio of 0 combo/100 combo flashlight radius.
ratingMultiplier *= 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);
// It is important to consider accuracy difficulty when scaling with accuracy.
ratingMultiplier *= 0.98 + Math.Pow(Math.Max(0, overallDifficulty), 2) / 2500;
return flashlightRating * Math.Sqrt(ratingMultiplier);
}
private double calculateAimVisibilityFactor(double approachRate)
{
const double ar_factor_end_point = 11.5;
double mechanicalDifficultyFactor = DifficultyCalculationUtils.ReverseLerp(mechanicalDifficultyRating, 5, 10);
double arFactorStartingPoint = double.Lerp(9, 10.33, mechanicalDifficultyFactor);
return DifficultyCalculationUtils.ReverseLerp(approachRate, ar_factor_end_point, arFactorStartingPoint);
}
private double calculateSpeedVisibilityFactor(double approachRate)
{
const double ar_factor_end_point = 11.5;
double mechanicalDifficultyFactor = DifficultyCalculationUtils.ReverseLerp(mechanicalDifficultyRating, 5, 10);
double arFactorStartingPoint = double.Lerp(10, 10.33, mechanicalDifficultyFactor);
return DifficultyCalculationUtils.ReverseLerp(approachRate, ar_factor_end_point, arFactorStartingPoint);
}
private static double calculateMechanicalDifficultyRating(double aimDifficultyValue, double speedDifficultyValue)
{
double aimValue = OsuStrainSkill.DifficultyToPerformance(calculateDifficultyRating(aimDifficultyValue));
double speedValue = OsuStrainSkill.DifficultyToPerformance(calculateDifficultyRating(speedDifficultyValue));
double aimValue = OsuStrainSkill.DifficultyToPerformance(OsuRatingCalculator.CalculateDifficultyRating(aimDifficultyValue));
double speedValue = OsuStrainSkill.DifficultyToPerformance(OsuRatingCalculator.CalculateDifficultyRating(speedDifficultyValue));
double totalValue = Math.Pow(Math.Pow(aimValue, 1.1) + Math.Pow(speedValue, 1.1), 1 / 1.1);
@@ -345,8 +177,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty
return Math.Cbrt(multiplier) * star_rating_multiplier * (Math.Cbrt(100000 / Math.Pow(2, 1 / 1.1) * basePerformance) + 4);
}
private static double calculateDifficultyRating(double difficultyValue) => Math.Sqrt(difficultyValue) * difficulty_multiplier;
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate)
{
List<DifficultyHitObject> objects = new List<DifficultyHitObject>();
@@ -209,7 +209,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
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 OsuModTraceable))
{
aimValue *= 1.0 + OsuDifficultyCalculator.CalculateVisibilityBonus(score.Mods, approachRate);
aimValue *= 1.0 + OsuRatingCalculator.CalculateVisibilityBonus(score.Mods, approachRate);
}
aimValue *= accuracy;
@@ -245,7 +245,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
}
else if (score.Mods.Any(m => m is OsuModTraceable))
{
speedValue *= 1.0 + OsuDifficultyCalculator.CalculateVisibilityBonus(score.Mods, approachRate);
speedValue *= 1.0 + OsuRatingCalculator.CalculateVisibilityBonus(score.Mods, approachRate);
}
double speedHighDeviationMultiplier = calculateSpeedHighDeviationNerf(attributes);
@@ -0,0 +1,199 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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.Rulesets.Difficulty.Utils;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Mods;
namespace osu.Game.Rulesets.Osu.Difficulty
{
public class OsuRatingCalculator
{
private const double difficulty_multiplier = 0.0675;
private readonly Mod[] mods;
private readonly int totalHits;
private readonly double approachRate;
private readonly double overallDifficulty;
private readonly double mechanicalDifficultyRating;
public OsuRatingCalculator(Mod[] mods, int totalHits, double approachRate, double overallDifficulty, double mechanicalDifficultyRating)
{
this.mods = mods;
this.totalHits = totalHits;
this.approachRate = approachRate;
this.overallDifficulty = overallDifficulty;
this.mechanicalDifficultyRating = mechanicalDifficultyRating;
}
public double ComputeAimRating(double aimDifficultyValue)
{
if (mods.Any(m => m is OsuModAutopilot))
return 0;
double aimRating = CalculateDifficultyRating(aimDifficultyValue);
if (mods.Any(m => m is OsuModTouchDevice))
aimRating = Math.Pow(aimRating, 0.8);
if (mods.Any(m => m is OsuModRelax))
aimRating *= 0.9;
if (mods.Any(m => m is OsuModMagnetised))
{
float magnetisedStrength = mods.OfType<OsuModMagnetised>().First().AttractionStrength.Value;
aimRating *= 1.0 - magnetisedStrength;
}
double ratingMultiplier = 1.0;
double approachRateLengthBonus = 0.95 + 0.4 * Math.Min(1.0, totalHits / 2000.0) +
(totalHits > 2000 ? Math.Log10(totalHits / 2000.0) * 0.5 : 0.0);
double approachRateFactor = 0.0;
if (approachRate > 10.33)
approachRateFactor = 0.3 * (approachRate - 10.33);
else if (approachRate < 8.0)
approachRateFactor = 0.05 * (8.0 - approachRate);
if (mods.Any(h => h is OsuModRelax))
approachRateFactor = 0.0;
ratingMultiplier *= 1.0 + approachRateFactor * approachRateLengthBonus; // Buff for longer maps with high AR.
if (mods.Any(m => m is OsuModHidden))
{
double visibilityFactor = calculateAimVisibilityFactor(approachRate);
ratingMultiplier *= 1.0 + CalculateVisibilityBonus(mods, approachRate, visibilityFactor);
}
// It is important to consider accuracy difficulty when scaling with accuracy.
ratingMultiplier *= 0.98 + Math.Pow(Math.Max(0, overallDifficulty), 2) / 2500;
return aimRating * Math.Cbrt(ratingMultiplier);
}
public double ComputeSpeedRating(double speedDifficultyValue)
{
if (mods.Any(m => m is OsuModRelax))
return 0;
double speedRating = CalculateDifficultyRating(speedDifficultyValue);
if (mods.Any(m => m is OsuModAutopilot))
speedRating *= 0.5;
if (mods.Any(m => m is OsuModMagnetised))
{
// reduce speed rating because of the speed distance scaling, with maximum reduction being 0.7x
float magnetisedStrength = mods.OfType<OsuModMagnetised>().First().AttractionStrength.Value;
speedRating *= 1.0 - magnetisedStrength * 0.3;
}
double ratingMultiplier = 1.0;
double approachRateLengthBonus = 0.95 + 0.4 * Math.Min(1.0, totalHits / 2000.0) +
(totalHits > 2000 ? Math.Log10(totalHits / 2000.0) * 0.5 : 0.0);
double approachRateFactor = 0.0;
if (approachRate > 10.33)
approachRateFactor = 0.3 * (approachRate - 10.33);
if (mods.Any(m => m is OsuModAutopilot))
approachRateFactor = 0.0;
ratingMultiplier *= 1.0 + approachRateFactor * approachRateLengthBonus; // Buff for longer maps with high AR.
if (mods.Any(m => m is OsuModHidden))
{
double visibilityFactor = calculateSpeedVisibilityFactor(approachRate);
ratingMultiplier *= 1.0 + CalculateVisibilityBonus(mods, approachRate, visibilityFactor);
}
ratingMultiplier *= 0.95 + Math.Pow(Math.Max(0, overallDifficulty), 2) / 750;
return speedRating * Math.Cbrt(ratingMultiplier);
}
public double ComputeFlashlightRating(double flashlightDifficultyValue)
{
if (!mods.Any(m => m is OsuModFlashlight))
return 0;
double flashlightRating = CalculateDifficultyRating(flashlightDifficultyValue);
if (mods.Any(m => m is OsuModTouchDevice))
flashlightRating = Math.Pow(flashlightRating, 0.8);
if (mods.Any(m => m is OsuModRelax))
flashlightRating *= 0.7;
else if (mods.Any(m => m is OsuModAutopilot))
flashlightRating *= 0.4;
if (mods.Any(m => m is OsuModMagnetised))
{
float magnetisedStrength = mods.OfType<OsuModMagnetised>().First().AttractionStrength.Value;
flashlightRating *= 1.0 - magnetisedStrength;
}
double ratingMultiplier = 1.0;
// Account for shorter maps having a higher ratio of 0 combo/100 combo flashlight radius.
ratingMultiplier *= 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);
// It is important to consider accuracy difficulty when scaling with accuracy.
ratingMultiplier *= 0.98 + Math.Pow(Math.Max(0, overallDifficulty), 2) / 2500;
return flashlightRating * Math.Sqrt(ratingMultiplier);
}
private double calculateAimVisibilityFactor(double approachRate)
{
const double ar_factor_end_point = 11.5;
double mechanicalDifficultyFactor = DifficultyCalculationUtils.ReverseLerp(mechanicalDifficultyRating, 5, 10);
double arFactorStartingPoint = double.Lerp(9, 10.33, mechanicalDifficultyFactor);
return DifficultyCalculationUtils.ReverseLerp(approachRate, ar_factor_end_point, arFactorStartingPoint);
}
private double calculateSpeedVisibilityFactor(double approachRate)
{
const double ar_factor_end_point = 11.5;
double mechanicalDifficultyFactor = DifficultyCalculationUtils.ReverseLerp(mechanicalDifficultyRating, 5, 10);
double arFactorStartingPoint = double.Lerp(10, 10.33, mechanicalDifficultyFactor);
return DifficultyCalculationUtils.ReverseLerp(approachRate, ar_factor_end_point, arFactorStartingPoint);
}
/// <summary>
/// Calculates a visibility bonus that is applicable to Hidden and Traceable.
/// </summary>
public static double CalculateVisibilityBonus(Mod[] mods, double approachRate, double visibilityFactor = 1)
{
// NOTE: TC's effect is only noticeable in performance calculations until lazer mods are accounted for server-side.
bool isAlwaysPartiallyVisible = mods.OfType<OsuModHidden>().Any(m => m.OnlyFadeApproachCircles.Value) || mods.OfType<OsuModTraceable>().Any();
// Start from normal curve, rewarding lower AR up to AR5
double readingBonus = 0.04 * (12.0 - Math.Max(approachRate, 5));
readingBonus *= visibilityFactor;
// For AR up to 0 - reduce reward for very low ARs when object is visible
if (approachRate < 5)
readingBonus += (isAlwaysPartiallyVisible ? 0.03 : 0.04) * (5.0 - Math.Max(approachRate, 0));
// Starting from AR0 - cap values so they won't grow to infinity
if (approachRate < 0)
readingBonus += (isAlwaysPartiallyVisible ? 0.075 : 0.1) * (1 - Math.Pow(1.5, approachRate));
return readingBonus;
}
public static double CalculateDifficultyRating(double difficultyValue) => Math.Sqrt(difficultyValue) * difficulty_multiplier;
}
}