From 28d36dd3bd6d50e713d05bd5ee8ffdd454b4be8e Mon Sep 17 00:00:00 2001 From: James Wilson Date: Fri, 25 Jul 2025 16:47:21 +0100 Subject: [PATCH] Move rating calculations to `OsuRatingCalculator` (#33265) * Move rating calculations to `OsuRatingCalculator` * Use `CalculateDifficultyRating` --- .../Difficulty/OsuDifficultyCalculator.cs | 200 ++---------------- .../Difficulty/OsuPerformanceCalculator.cs | 4 +- .../Difficulty/OsuRatingCalculator.cs | 199 +++++++++++++++++ 3 files changed, 216 insertions(+), 187 deletions(-) create mode 100644 osu.Game.Rulesets.Osu/Difficulty/OsuRatingCalculator.cs diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs index 513352825f..337bda3221 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs @@ -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; } - /// - /// Calculates a visibility bonus that is applicable to Hidden and Traceable. - /// - 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().Any(m => m.OnlyFadeApproachCircles.Value) || mods.OfType().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().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().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().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 CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) { List objects = new List(); diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index 49626eb7b6..11e9714ed8 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -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); diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuRatingCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuRatingCalculator.cs new file mode 100644 index 0000000000..e505ed07e4 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuRatingCalculator.cs @@ -0,0 +1,199 @@ +// 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.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().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().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().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); + } + + /// + /// Calculates a visibility bonus that is applicable to Hidden and Traceable. + /// + 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().Any(m => m.OnlyFadeApproachCircles.Value) || mods.OfType().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; + } +}