1
0
mirror of https://github.com/ppy/osu.git synced 2026-05-27 23:40:45 +08:00

Move remaining osu! rating adjustments to skills (#37623)

Co-authored-by: James Wilson <tsunyoku@gmail.com>
This commit is contained in:
StanR
2026-05-19 17:24:50 +05:00
committed by GitHub
Unverified
parent c0c952ef0e
commit 9a734d4de3
9 changed files with 78 additions and 114 deletions
@@ -15,8 +15,6 @@ using osu.Game.Rulesets.Osu.Difficulty.Skills;
using osu.Game.Rulesets.Osu.Difficulty.Utils;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Scoring;
using osu.Game.Rulesets.Scoring;
using osu.Game.Utils;
namespace osu.Game.Rulesets.Osu.Difficulty
@@ -30,22 +28,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty
{
}
public static double CalculateRateAdjustedApproachRate(double approachRate, double clockRate)
{
double preempt = IBeatmapDifficultyInfo.DifficultyRange(approachRate, OsuHitObject.PREEMPT_MAX, OsuHitObject.PREEMPT_MID, OsuHitObject.PREEMPT_MIN) / clockRate;
return IBeatmapDifficultyInfo.InverseDifficultyRange(preempt, OsuHitObject.PREEMPT_MAX, OsuHitObject.PREEMPT_MID, OsuHitObject.PREEMPT_MIN);
}
public static double CalculateRateAdjustedOverallDifficulty(double overallDifficulty, double clockRate)
{
HitWindows hitWindows = new OsuHitWindows();
hitWindows.SetDifficulty(overallDifficulty);
double hitWindowGreat = hitWindows.WindowFor(HitResult.Great) / clockRate;
return (79.5 - hitWindowGreat) / 6;
}
protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills)
{
if (beatmap.HitObjects.Count == 0)
@@ -78,8 +60,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty
double difficultSliders = aim.GetDifficultSliders();
double overallDifficulty = CalculateRateAdjustedOverallDifficulty(beatmap.Difficulty.OverallDifficulty, ModUtils.CalculateRateWithMods(mods));
int hitCircleCount = beatmap.HitObjects.Count(h => h is HitCircle);
int sliderCount = beatmap.HitObjects.Count(h => h is Slider);
int spinnerCount = beatmap.HitObjects.Count(h => h is Spinner);
@@ -87,19 +67,17 @@ namespace osu.Game.Rulesets.Osu.Difficulty
int totalHits = beatmap.HitObjects.Count;
double sliderFactor = aimDifficultyValue > 0
? OsuRatingCalculator.CalculateDifficultyRating(aimNoSlidersDifficultyValue) / OsuRatingCalculator.CalculateDifficultyRating(aimDifficultyValue)
? calculateDifficultyRating(aimNoSlidersDifficultyValue) / calculateDifficultyRating(aimDifficultyValue) // TODO: this is intentionally left incorrect
: 1;
var osuRatingCalculator = new OsuRatingCalculator(totalHits, overallDifficulty);
double aimRating = osuRatingCalculator.ComputeAimRating(aimDifficultyValue);
double speedRating = osuRatingCalculator.ComputeSpeedRating(speedDifficultyValue);
double readingRating = osuRatingCalculator.ComputeReadingRating(readingDifficultyValue);
double aimRating = calculateAimDifficultyRating(aimDifficultyValue);
double speedRating = calculateDifficultyRating(speedDifficultyValue);
double readingRating = calculateDifficultyRating(readingDifficultyValue);
double flashlightRating = 0.0;
if (flashlight is not null)
flashlightRating = osuRatingCalculator.ComputeFlashlightRating(flashlight.DifficultyValue());
flashlightRating = calculateDifficultyRating(flashlight.DifficultyValue());
double sliderNestedScorePerObject = LegacyScoreUtils.CalculateNestedScorePerObject(beatmap, totalHits);
double legacyScoreBaseMultiplier = LegacyScoreUtils.CalculateDifficultyPeppyStars(WorkingBeatmap.Beatmap);
@@ -157,6 +135,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty
return DifficultyCalculationUtils.Norm(OsuPerformanceCalculator.PERFORMANCE_NORM_EXPONENT, reading, flashlight * Math.Clamp(flashlight / reading, 0.25, 1.0));
}
private double calculateAimDifficultyRating(double difficultyValue) => Math.Pow(difficultyValue, 0.63) * 0.02275;
private double calculateDifficultyRating(double difficultyValue) => Math.Sqrt(difficultyValue) * 0.0675;
private double calculateStarRating(double basePerformance)
{
return Math.Cbrt(basePerformance * OsuPerformanceCalculator.PERFORMANCE_BASE_MULTIPLIER);
@@ -189,7 +171,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
};
if (mods.Any(h => h is OsuModFlashlight))
skills.Add(new Flashlight(mods));
skills.Add(new Flashlight(mods, beatmap.HitObjects.Count));
return skills.ToArray();
}
@@ -5,13 +5,15 @@ using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Game.Rulesets.Difficulty.Utils;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Scoring;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Difficulty.Skills;
using osu.Game.Rulesets.Difficulty.Utils;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Difficulty.Skills;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Scoring;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Utils;
@@ -99,8 +101,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty
okHitWindow = hitWindows.WindowFor(HitResult.Ok) / clockRate;
mehHitWindow = hitWindows.WindowFor(HitResult.Meh) / clockRate;
approachRate = OsuDifficultyCalculator.CalculateRateAdjustedApproachRate(difficulty.ApproachRate, clockRate);
overallDifficulty = OsuDifficultyCalculator.CalculateRateAdjustedOverallDifficulty(difficulty.OverallDifficulty, clockRate);
approachRate = calculateRateAdjustedApproachRate(difficulty.ApproachRate, clockRate);
overallDifficulty = (79.5 - greatHitWindow) / 6;
drainRate = difficulty.DrainRate;
double comboBasedEstimatedMissCount = calculateComboBasedEstimatedMissCount(osuAttributes);
@@ -536,6 +538,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty
private double calculateMissPenalty(double missCount, double difficultStrainCount) => 0.93 / (missCount / (4 * Math.Log(difficultStrainCount)) + 1);
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 double calculateRateAdjustedApproachRate(double approachRate, double clockRate)
{
double preempt = IBeatmapDifficultyInfo.DifficultyRange(approachRate, OsuHitObject.PREEMPT_MAX, OsuHitObject.PREEMPT_MID, OsuHitObject.PREEMPT_MIN) / clockRate;
return IBeatmapDifficultyInfo.InverseDifficultyRange(preempt, OsuHitObject.PREEMPT_MAX, OsuHitObject.PREEMPT_MID, OsuHitObject.PREEMPT_MIN);
}
private int totalHits => countGreat + countOk + countMeh + countMiss;
private int totalSuccessfulHits => countGreat + countOk + countMeh;
private int totalImperfectHits => countOk + countMeh + countMiss;
@@ -1,67 +0,0 @@
// 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;
namespace osu.Game.Rulesets.Osu.Difficulty
{
public class OsuRatingCalculator
{
private const double difficulty_multiplier = 0.0675;
private readonly int totalHits;
private readonly double overallDifficulty;
public OsuRatingCalculator(int totalHits, double overallDifficulty)
{
this.totalHits = totalHits;
this.overallDifficulty = overallDifficulty;
}
public double ComputeAimRating(double aimDifficultyValue)
{
double aimRating = Math.Pow(aimDifficultyValue, 0.63) * 0.02275;
double ratingMultiplier = 1.0;
// 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)
{
return CalculateDifficultyRating(speedDifficultyValue);
}
public double ComputeReadingRating(double readingDifficultyValue)
{
double readingRating = CalculateDifficultyRating(readingDifficultyValue);
double ratingMultiplier = 1.0;
ratingMultiplier *= 0.75 + Math.Pow(Math.Max(0, overallDifficulty), 2.2) / 800;
return readingRating * Math.Cbrt(ratingMultiplier);
}
public double ComputeFlashlightRating(double flashlightDifficultyValue)
{
double flashlightRating = CalculateDifficultyRating(flashlightDifficultyValue);
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);
}
public static double CalculateDifficultyRating(double difficultyValue) => Math.Sqrt(difficultyValue) * difficulty_multiplier;
}
}
@@ -128,6 +128,19 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
/// </summary>
public double SmallCircleBonus { get; private set; }
/// <summary>
/// Object's immediate OverallDifficulty value calculated from the raw hitwindow.
/// </summary>
public double OverallDifficulty
{
get
{
double hitWindowGreat = RawHitWindow(HitResult.Great) / ClockRate;
return (79.5 - hitWindowGreat) / 6;
}
}
private readonly OsuDifficultyHitObject? lastLastDifficultyObject;
private readonly OsuDifficultyHitObject? lastDifficultyObject;
@@ -63,7 +63,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
double decay = strainDecay(((OsuDifficultyHitObject)current).AdjustedDeltaTime);
currentStrain *= decay;
currentStrain += calculateModAdjustedDifficulty(current) * (1 - decay);
currentStrain += calculateAdjustedDifficulty(current) * (1 - decay);
if (current.BaseObject is Slider)
sliderStrains.Add(currentStrain);
@@ -71,7 +71,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
return currentStrain;
}
private double calculateModAdjustedDifficulty(DifficultyHitObject current)
private double calculateAdjustedDifficulty(DifficultyHitObject current)
{
double snapDifficulty = SnapAimEvaluator.EvaluateDifficultyOf(current, IncludeSliders) * skillMultiplierSnap;
double agilityDifficulty = AgilityEvaluator.EvaluateDifficultyOf(current) * skillMultiplierAgility;
@@ -85,6 +85,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
totalDifficulty *= 1.0 - magnetisedStrength;
}
totalDifficulty *= 0.985 + Math.Pow(Math.Max(0, ((OsuDifficultyHitObject)current).OverallDifficulty), 2) / 4000;
return totalDifficulty;
}
@@ -8,6 +8,7 @@ using osu.Game.Rulesets.Difficulty.Skills;
using osu.Game.Rulesets.Difficulty.Utils;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Difficulty.Evaluators;
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
using osu.Game.Rulesets.Osu.Mods;
namespace osu.Game.Rulesets.Osu.Difficulty.Skills
@@ -17,9 +18,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
/// </summary>
public class Flashlight : StrainSkill
{
public Flashlight(Mod[] mods)
private readonly int totalObjects;
public Flashlight(Mod[] mods, int totalObjects)
: base(mods)
{
this.totalObjects = totalObjects;
}
private double skillMultiplier => 0.058;
@@ -37,12 +41,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
return 0;
currentStrain *= strainDecay(current.DeltaTime);
currentStrain += calculateModAdjustedDifficulty(current) * skillMultiplier;
currentStrain += calculateAdjustedDifficulty(current) * skillMultiplier;
return currentStrain;
}
private double calculateModAdjustedDifficulty(DifficultyHitObject current)
private double calculateAdjustedDifficulty(DifficultyHitObject current)
{
double difficulty = FlashlightEvaluator.EvaluateDifficultyOf(current, Mods);
@@ -67,10 +71,21 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
if (Mods.Any(m => m is OsuModAutopilot))
difficulty *= 0.4;
difficulty *= 0.985 + Math.Pow(Math.Max(0, ((OsuDifficultyHitObject)current).OverallDifficulty), 2) / 4000;
return difficulty;
}
public override double DifficultyValue() => GetCurrentStrainPeaks().Sum();
public override double DifficultyValue()
{
double sum = GetCurrentStrainPeaks().Sum();
// Account for shorter maps having a higher ratio of 0 combo/100 combo flashlight radius.
sum *= 0.7 + 0.1 * Math.Min(1.0, totalObjects / 200.0) +
(totalObjects > 200 ? 0.2 * Math.Min(1.0, (totalObjects - 200) / 200.0) : 0.0);
return sum;
}
public static double DifficultyToPerformance(double difficulty) => 25 * Math.Pow(difficulty, 2);
}
@@ -10,6 +10,7 @@ using osu.Game.Rulesets.Difficulty.Skills;
using osu.Game.Rulesets.Difficulty.Utils;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Difficulty.Evaluators;
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
using osu.Game.Rulesets.Osu.Mods;
namespace osu.Game.Rulesets.Osu.Difficulty.Skills
@@ -40,12 +41,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
double decay = strainDecay(current.DeltaTime);
currentStrain *= decay;
currentStrain += calculateModAdjustedDifficulty(current) * (1 - decay) * skillMultiplier;
currentStrain += calculateAdjustedDifficulty(current) * (1 - decay) * skillMultiplier;
return currentStrain;
}
private double calculateModAdjustedDifficulty(DifficultyHitObject current)
private double calculateAdjustedDifficulty(DifficultyHitObject current)
{
double difficulty = ReadingEvaluator.EvaluateDifficultyOf(current, hasHiddenMod);
@@ -64,6 +65,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
if (Mods.Any(m => m is OsuModAutopilot))
difficulty *= 0.1;
difficulty *= 0.825 + Math.Pow(Math.Max(0, ((OsuDifficultyHitObject)current).OverallDifficulty), 2.2) / 1125.0;
return difficulty;
}
@@ -46,7 +46,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
double decay = strainDecay(((OsuDifficultyHitObject)current).AdjustedDeltaTime);
currentStrain *= decay;
currentStrain += calculateModAdjustedDifficulty(current) * (1 - decay) * skillMultiplier;
currentStrain += calculateAdjustedDifficulty(current) * (1 - decay) * skillMultiplier;
double currentRhythm = RhythmEvaluator.EvaluateDifficultyOf(current);
@@ -58,7 +58,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
return totalStrain;
}
private double calculateModAdjustedDifficulty(DifficultyHitObject current)
private double calculateAdjustedDifficulty(DifficultyHitObject current)
{
double difficulty = SpeedEvaluator.EvaluateDifficultyOf(current);
@@ -84,9 +84,17 @@ namespace osu.Game.Rulesets.Difficulty.Preprocessing
}
/// <summary>
/// Retrieves the full hit window for a <see cref="HitResult"/>.
/// Retrieves the full rate-adjusted hit window for a <see cref="HitResult"/>.
/// </summary>
public virtual double HitWindow(HitResult hitResult)
public double HitWindow(HitResult hitResult)
{
return 2 * RawHitWindow(hitResult) / ClockRate;
}
/// <summary>
/// Retrieves the hit window for a <see cref="HitResult"/>.
/// </summary>
protected virtual double RawHitWindow(HitResult hitResult)
{
// Try to get HitWindows from nested hit objects
// This is important for objects such as Slider in osu! where the object itself has HitWindows set to Empty, but the nested SliderHead has proper hit windows
@@ -97,11 +105,11 @@ namespace osu.Game.Rulesets.Difficulty.Preprocessing
if (nestedHitObject.HitWindows == HitWindows.Empty)
continue;
return 2 * nestedHitObject.HitWindows.WindowFor(hitResult) / ClockRate;
return nestedHitObject.HitWindows.WindowFor(hitResult);
}
}
return 2 * BaseObject.HitWindows.WindowFor(hitResult) / ClockRate;
return BaseObject.HitWindows.WindowFor(hitResult);
}
}
}