1
0
mirror of https://github.com/ppy/osu.git synced 2025-02-13 06:22:55 +08:00

Big amount of changes

1) Fully remade HD calc: now it's strain-based
2) Remade high AR calc: now it's using more correct aim-speed summing
3) Added explicit nerf for fiery patterns
4) Fixed bug where HR pop-offing slideraim difficulty due to sliderend position not being mirrored (no longer Rat Race +50)
5) Splitted some files cuz it's more convenient for me to edit
This commit is contained in:
Givikap120 2024-02-02 21:15:05 +02:00
parent ba265ac2d9
commit 1a68e29a95
11 changed files with 671 additions and 334 deletions

View File

@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
/// <item><description>and slider difficulty.</description></item>
/// </list>
/// </summary>
public static double EvaluateDifficultyOf(DifficultyHitObject current, bool withSliderTravelDistance, double clampPreemptTime = 0)
public static double EvaluateDifficultyOf(DifficultyHitObject current, bool withSliderTravelDistance)
{
if (current.BaseObject is Spinner || current.Index <= 1 || current.Previous(0).BaseObject is Spinner)
return 0;
@ -121,17 +121,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
// Add in acute angle bonus or wide angle bonus + velocity change bonus, whichever is larger.
aimStrain += Math.Max(acuteAngleBonus * acute_angle_multiplier, wideAngleBonus * wide_angle_multiplier + velocityChangeBonus * velocity_change_multiplier);
if (clampPreemptTime > 0)
{
// Scale if AR is too high for high AR calc
double multiplier = osuCurrObj.StrainTime / Math.Min(osuCurrObj.StrainTime, clampPreemptTime - 150); // 150ms is considered as reaction time
double multiplierIfAR11 = osuCurrObj.StrainTime / Math.Min(osuCurrObj.StrainTime, 150);
multiplier = Math.Min(multiplier, multiplierIfAR11);
aimStrain *= multiplier;
}
// Add in additional slider velocity bonus.
if (withSliderTravelDistance)
aimStrain += sliderBonus * slider_multiplier;

View File

@ -11,6 +11,7 @@ using osu.Game.Rulesets.Osu.Objects;
namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
{
// Main class with some util functions
public static class ReadingEvaluator
{
private const double reading_window_size = 3000;
@ -26,7 +27,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
double loopDifficulty = currObj.OpacityAt(loopObj.BaseObject.StartTime, false);
// Small distances means objects may be cheesed, so it doesn't matter whether they are arranged confusingly.
loopDifficulty *= logistic((loopObj.MinimumJumpDistance - 90) / 15);
loopDifficulty *= logistic((loopObj.MinimumJumpDistance - 60) / 10);
//double timeBetweenCurrAndLoopObj = (currObj.BaseObject.StartTime - loopObj.BaseObject.StartTime) / clockRateEstimate;
double timeBetweenCurrAndLoopObj = currObj.StartTime - loopObj.StartTime;
@ -79,57 +80,13 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
return difficulty;
}
public static double EvaluateHighARDifficultyOf(DifficultyHitObject current, bool applyAdjust = false)
{
var currObj = (OsuDifficultyHitObject)current;
double result = highArCurve(currObj.Preempt);
if (applyAdjust)
{
double inpredictability = EvaluateInpredictabilityOf(current);
// follow lines make high AR easier, so apply nerf if object isn't new combo
inpredictability *= 1 + 0.1 * (800 - currObj.FollowLineTime) / 800;
result *= 0.85 + 0.75 * inpredictability;
}
return result;
}
public static double EvaluateHiddenDifficultyOf(DifficultyHitObject current)
{
var currObj = (OsuDifficultyHitObject)current;
double aimDifficulty = AimEvaluator.EvaluateDifficultyOf(current, false);
double hdDifficulty = 0;
double timeSpentInvisible = getDurationSpentInvisible(currObj) / currObj.ClockRate;
double density = 1 + Math.Max(0, CalculateDenstityOf(currObj) - 1);
density *= getConstantAngleNerfFactor(currObj);
double timeDifficultyFactor = density / 1000;
double visibleObjectFactor = Math.Clamp(retrieveCurrentVisibleObjects(currObj).Count - 2, 0, 15);
hdDifficulty += Math.Pow(visibleObjectFactor * timeSpentInvisible * timeDifficultyFactor, 1) +
(6 + visibleObjectFactor) * aimDifficulty;
hdDifficulty *= 0.95 + 0.15 * EvaluateInpredictabilityOf(current); // Max multiplier is 1.1
return hdDifficulty;
}
// Returns value from 0 to 1, where 0 is very predictable and 1 is very unpredictable
public static double EvaluateInpredictabilityOf(DifficultyHitObject current)
{
// make the sum equal to 1
const double velocity_change_part = 0.3;
const double angle_change_part = 0.6;
const double rhythm_change_part = 0.1;
const double velocity_change_part = 0.25;
const double angle_change_part = 0.45;
const double rhythm_change_part = 0.3;
if (current.BaseObject is Spinner || current.Index == 0 || current.Previous(0).BaseObject is Spinner)
return 0;
@ -137,23 +94,19 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
var osuCurrObj = (OsuDifficultyHitObject)current;
var osuLastObj = (OsuDifficultyHitObject)current.Previous(0);
double velocityChangeBonus = 0;
// Rhythm difference punishment for velocity and angle bonuses
double rhythmSimilarity = 1 - getRhythmDifference(osuCurrObj.StrainTime, osuLastObj.StrainTime);
// Make differentiation going from 1/4 to 1/2 and bigger difference
// To 1/3 to 1/2 and smaller difference
rhythmSimilarity = Math.Clamp(rhythmSimilarity, 0.5, 0.75);
rhythmSimilarity = 4 * (rhythmSimilarity - 0.5);
double velocityChangeBonus = getVelocityChangeFactor(osuCurrObj, osuLastObj) * rhythmSimilarity;
double currVelocity = osuCurrObj.LazyJumpDistance / osuCurrObj.StrainTime;
double prevVelocity = osuLastObj.LazyJumpDistance / osuLastObj.StrainTime;
// https://www.desmos.com/calculator/kqxmqc8pkg
if (currVelocity > 0 || prevVelocity > 0)
{
double velocityChange = Math.Max(0,
Math.Min(
Math.Abs(prevVelocity - currVelocity) - 0.5 * Math.Min(currVelocity, prevVelocity),
Math.Max(((OsuHitObject)osuCurrObj.BaseObject).Radius / Math.Max(osuCurrObj.StrainTime, osuLastObj.StrainTime), Math.Min(currVelocity, prevVelocity))
)); // Stealed from xexxar
velocityChangeBonus = velocityChange / Math.Max(currVelocity, prevVelocity); // maxiumum is 0.4
velocityChangeBonus /= 0.4;
}
double angleChangeBonus = 0;
if (osuCurrObj.Angle != null && osuLastObj.Angle != null && currVelocity > 0 && prevVelocity > 0)
@ -162,6 +115,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
angleChangeBonus *= Math.Min(currVelocity, prevVelocity) / Math.Max(currVelocity, prevVelocity); // Prevent cheesing
}
angleChangeBonus *= rhythmSimilarity;
// This bonus only awards rhythm changes if they're not filled with sliderends
double rhythmChangeBonus = 0;
if (current.Index > 1)
@ -190,19 +146,26 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
return result;
}
public static double EvaluateLowDensityBonusOf(DifficultyHitObject current)
private static double getVelocityChangeFactor(OsuDifficultyHitObject osuCurrObj, OsuDifficultyHitObject osuLastObj)
{
//var currObj = (OsuDifficultyHitObject)current;
double currVelocity = osuCurrObj.LazyJumpDistance / osuCurrObj.StrainTime;
double prevVelocity = osuLastObj.LazyJumpDistance / osuLastObj.StrainTime;
//// Density = 2 in general means 3 notes on screen (it's not including current note)
//double density = CalculateDenstityOf(currObj);
double velocityChangeFactor = 0;
//// We are considering density = 1.5 as starting point, 1.0 is noticably uncomfy and 0.5 is severely uncomfy
//double bonus = 1.5 - density;
//if (bonus <= 0) return 0;
// https://www.desmos.com/calculator/kqxmqc8pkg
if (currVelocity > 0 || prevVelocity > 0)
{
double velocityChange = Math.Max(0,
Math.Min(
Math.Abs(prevVelocity - currVelocity) - 0.5 * Math.Min(currVelocity, prevVelocity),
Math.Max(((OsuHitObject)osuCurrObj.BaseObject).Radius / Math.Max(osuCurrObj.StrainTime, osuLastObj.StrainTime), Math.Min(currVelocity, prevVelocity))
)); // Stealed from xexxar
velocityChangeFactor = velocityChange / Math.Max(currVelocity, prevVelocity); // maxiumum is 0.4
velocityChangeFactor /= 0.4;
}
//return Math.Pow(bonus, 2);
return 0;
return velocityChangeFactor;
}
// Returns a list of objects that are visible on screen at
@ -222,35 +185,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
}
}
private static List<OsuDifficultyHitObject> retrieveCurrentVisibleObjects(OsuDifficultyHitObject current)
{
List<OsuDifficultyHitObject> objects = new List<OsuDifficultyHitObject>();
for (int i = 0; i < current.Count; i++)
{
OsuDifficultyHitObject hitObject = (OsuDifficultyHitObject)current.Next(i);
if (hitObject.IsNull() ||
(hitObject.StartTime - current.StartTime) > reading_window_size ||
current.StartTime < hitObject.StartTime - hitObject.Preempt)
break;
objects.Add(hitObject);
}
return objects;
}
private static double getDurationSpentInvisible(OsuDifficultyHitObject current)
{
var baseObject = (OsuHitObject)current.BaseObject;
double fadeOutStartTime = baseObject.StartTime - baseObject.TimePreempt + baseObject.TimeFadeIn;
double fadeOutDuration = baseObject.TimePreempt * OsuModHidden.FADE_OUT_DURATION_MULTIPLIER;
return (fadeOutStartTime + fadeOutDuration) - (baseObject.StartTime - baseObject.TimePreempt);
}
private static double getConstantAngleNerfFactor(OsuDifficultyHitObject current)
{
const double time_limit = 2000;
@ -327,19 +261,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
return Math.Clamp(2 - deltaTime / (reading_window_size / 2), 0, 1);
}
// https://www.desmos.com/calculator/hbj7swzlth
private static double highArCurve(double preempt)
{
double value = Math.Pow(3, 3 - 0.01 * preempt); // 1 for 300ms, 0.25 for 400ms, 0.0625 for 500ms
value = softmin(value, 2, 1.7); // use softmin to achieve full-memory cap, 2 times more than AR11 (300ms)
return value;
}
private static double getRhythmDifference(double t1, double t2) => 1 - Math.Min(t1, t2) / Math.Max(t1, t2);
private static double logistic(double x) => 1 / (1 + Math.Exp(-x));
// We are using mutiply and divide instead of add and subtract, so values won't be negative
// https://www.desmos.com/calculator/fv5xerwpd2
private static double softmin(double a, double b, double power = Math.E) => a * b / Math.Log(Math.Pow(power, a) + Math.Pow(power, b), power);
}
}

View File

@ -0,0 +1,219 @@
// 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.Collections.Generic;
using System.Linq;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
{
// Class for HD calc. Split because there are a lot of things in HD calc.
public static class ReadingHiddenEvaluator
{
private const double reading_window_size = 3000;
public static double EvaluateDifficultyOf(DifficultyHitObject current)
{
var currObj = (OsuDifficultyHitObject)current;
double density = 0;
double densityAnglesNerf = -2; // we have threshold of 2, so 2 or same angles won't be punished
OsuDifficultyHitObject? prevObj0 = null;
OsuDifficultyHitObject? prevObj1 = null;
OsuDifficultyHitObject? prevObj2 = null;
double prevConstantAngle = 0;
foreach (var loopObj in retrievePastVisibleObjects(currObj).Reverse())
{
double loopDifficulty = currObj.OpacityAt(loopObj.BaseObject.StartTime, false);
// Small distances means objects may be cheesed, so it doesn't matter whether they are arranged confusingly.
// For HD: it's not subtracting anything cuz it's multiplied by the aim difficulty anyways.
// loopDifficulty *= logistic((loopObj.MinimumJumpDistance) / 15);
// Reduce density bonus for this object if they're too apart in time
// Nerf starts on 1500ms and reaches maximum (*=0) on 3000ms
double timeBetweenCurrAndLoopObj = currObj.StartTime - loopObj.StartTime;
loopDifficulty *= getTimeNerfFactor(timeBetweenCurrAndLoopObj);
if (prevObj0.IsNull())
{
prevObj0 = loopObj;
continue;
}
// HD-exclusive burst nerf
// Only if next object is slower, representing break from many notes in a row
if (loopObj.StrainTime > prevObj0.StrainTime)
{
// Get rhythm similarity: 1 on same rhythms, 0.5 on 1/4 to 1/2
double rhythmSimilarity = 1 - getRhythmDifference(loopObj.StrainTime, prevObj0.StrainTime);
// Make differentiation going from 1/4 to 1/2 and bigger difference
// To 1/3 to 1/2 and smaller difference
rhythmSimilarity = Math.Clamp(rhythmSimilarity, 0.5, 0.75);
rhythmSimilarity = 4 * (rhythmSimilarity - 0.5);
// Reduce density for this objects if rhythms are different
loopDifficulty *= rhythmSimilarity;
}
density += loopDifficulty;
// Angles nerf
if (loopObj.Angle.IsNotNull() && prevObj0.Angle.IsNotNull())
{
double angleDifference = Math.Abs(prevObj0.Angle.Value - loopObj.Angle.Value);
// Nerf alternating angles case
if (prevObj1.IsNotNull() && prevObj2.IsNotNull() && prevObj1.Angle.IsNotNull() && prevObj2.Angle.IsNotNull())
{
// Normalized difference
double angleDifference1 = Math.Abs(prevObj1.Angle.Value - loopObj.Angle.Value) / Math.PI;
double angleDifference2 = Math.Abs(prevObj2.Angle.Value - prevObj0.Angle.Value) / Math.PI;
// Will be close to 1 if angleDifference1 and angleDifference2 was both close to 0
double alternatingFactor = Math.Pow((1 - angleDifference1) * (1 - angleDifference2), 2);
// Be sure to nerf only same rhythms
double rhythmFactor = 1 - getRhythmDifference(loopObj.StrainTime, prevObj0.StrainTime); // 0 on different rhythm, 1 on same rhythm
rhythmFactor *= 1 - getRhythmDifference(prevObj0.StrainTime, prevObj1.StrainTime);
rhythmFactor *= 1 - getRhythmDifference(prevObj1.StrainTime, prevObj2.StrainTime);
double acuteAngleFactor = 1 - Math.Min(loopObj.Angle.Value, prevObj0.Angle.Value) / Math.PI;
double prevAngleAdjust = Math.Max(angleDifference - angleDifference1, 0);
prevAngleAdjust *= alternatingFactor; // Nerf if alternating
prevAngleAdjust *= rhythmFactor; // Nerf if same rhythms
prevAngleAdjust *= acuteAngleFactor;
angleDifference -= prevAngleAdjust;
}
// Reduce angles nerf if objects are too apart in time
// Angle nerf is starting being reduced from 200ms (150BPM jump) and it reduced to 0 on 2000ms
double longIntervalFactor = Math.Clamp(1 - (loopObj.StrainTime - 200) / (2000 - 200), 0, 1);
// Current angle nerf. Angle difference less than 15 degrees is considered the same
double currConstantAngle = Math.Cos(4 * Math.Min(Math.PI / 12, angleDifference)) * longIntervalFactor;
// Apply the nerf only when it's repeated
double currentAngleNerf = Math.Min(currConstantAngle, prevConstantAngle);
densityAnglesNerf += Math.Min(currentAngleNerf, loopDifficulty);
prevConstantAngle = currConstantAngle;
}
prevObj2 = prevObj1;
prevObj1 = prevObj0;
prevObj0 = loopObj;
}
// Apply angles nerf
density -= Math.Max(0, densityAnglesNerf);
// Consider that density matters only starting from 3rd note on the screen
double densityFactor = Math.Max(0, density - 1) / 4;
// This is kinda wrong cuz it returns value bigger than preempt
// double timeSpentInvisible = getDurationSpentInvisible(currObj) / 1000 / currObj.ClockRate;
// The closer timeSpentInvisible is to 0 -> the less difference there are between NM and HD
// So we will reduce base according to this
// It will be 0.354 on AR11 value
double invisibilityFactor = logistic(currObj.Preempt / 120 - 4);
double hdDifficulty = invisibilityFactor + densityFactor;
// Scale by inpredictability slightly
hdDifficulty *= 0.95 + 0.15 * ReadingEvaluator.EvaluateInpredictabilityOf(current); // Max multiplier is 1.1
return hdDifficulty;
}
//public static double EvaluateHiddenDifficultyOfOld(DifficultyHitObject current)
//{
// var currObj = (OsuDifficultyHitObject)current;
// double aimDifficulty = AimEvaluator.EvaluateDifficultyOf(current, false);
// double timeSpentInvisible = getDurationSpentInvisible(currObj) / currObj.ClockRate;
// double density = 1 + Math.Max(0, CalculateDenstityOf(currObj) - 1);
// double timeDifficultyFactor = density / 1000;
// timeDifficultyFactor *= getConstantAngleNerfFactor(currObj);
// double visibleObjectFactor = Math.Clamp(retrieveCurrentVisibleObjects(currObj).Count - 2, 0, 15);
// double hdDifficulty = visibleObjectFactor * timeSpentInvisible * timeDifficultyFactor +
// (6 + visibleObjectFactor) * aimDifficulty;
// hdDifficulty *= 0.95 + 0.15 * EvaluateInpredictabilityOf(current); // Max multiplier is 1.1
// return hdDifficulty;
//}
// Returns a list of objects that are visible on screen at
// the point in time at which the current object becomes visible.
private static IEnumerable<OsuDifficultyHitObject> retrievePastVisibleObjects(OsuDifficultyHitObject current)
{
for (int i = 0; i < current.Index; i++)
{
OsuDifficultyHitObject hitObject = (OsuDifficultyHitObject)current.Previous(i);
if (hitObject.IsNull() ||
current.StartTime - hitObject.StartTime > reading_window_size ||
hitObject.StartTime < current.StartTime - current.Preempt)
break;
yield return hitObject;
}
}
//private static double getDurationSpentInvisible(OsuDifficultyHitObject current)
//{
// var baseObject = (OsuHitObject)current.BaseObject;
// double fadeOutStartTime = baseObject.StartTime - baseObject.TimePreempt + baseObject.TimeFadeIn;
// double fadeOutDuration = baseObject.TimePreempt * OsuModHidden.FADE_OUT_DURATION_MULTIPLIER;
// return (fadeOutStartTime + fadeOutDuration) - (baseObject.StartTime - baseObject.TimePreempt);
//}
//private static List<OsuDifficultyHitObject> retrieveCurrentVisibleObjects(OsuDifficultyHitObject current)
//{
// List<OsuDifficultyHitObject> objects = new List<OsuDifficultyHitObject>();
// for (int i = 0; i < current.Count; i++)
// {
// OsuDifficultyHitObject hitObject = (OsuDifficultyHitObject)current.Next(i);
// if (hitObject.IsNull() ||
// (hitObject.StartTime - current.StartTime) > reading_window_size ||
// current.StartTime < hitObject.StartTime - hitObject.Preempt)
// break;
// objects.Add(hitObject);
// }
// return objects;
//}
private static double getTimeNerfFactor(double deltaTime)
{
return Math.Clamp(2 - deltaTime / (reading_window_size / 2), 0, 1);
}
private static double getRhythmDifference(double t1, double t2) => 1 - Math.Min(t1, t2) / Math.Max(t1, t2);
private static double logistic(double x) => 1 / (1 + Math.Exp(-x));
}
}

View File

@ -0,0 +1,149 @@
// 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.Collections.Generic;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Osu.Objects;
namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
{
// Main class with some util functions
public static class ReadingHighAREvaluator
{
public static double EvaluateDifficultyOf(DifficultyHitObject current, bool applyAdjust = false)
{
var currObj = (OsuDifficultyHitObject)current;
double result = highArCurve(currObj.Preempt);
if (applyAdjust)
{
double inpredictability = ReadingEvaluator.EvaluateInpredictabilityOf(current);
// follow lines make high AR easier, so apply nerf if object isn't new combo
inpredictability *= 1 + 0.1 * (800 - currObj.FollowLineTime) / 800;
result *= 0.85 + 1 * inpredictability;
result *= 1.05 - 0.4 * EvaluateFieryAnglePunishmentOf(current);
}
return result;
}
// Explicitely nerfs edgecased fiery-type jumps for high AR. The difference from Inpredictability is that this is not used in HD calc
public static double EvaluateFieryAnglePunishmentOf(DifficultyHitObject current)
{
if (current.Index <= 2)
return 0;
var currObj = (OsuDifficultyHitObject)current;
var lastObj0 = (OsuDifficultyHitObject)current.Previous(0);
var lastObj1 = (OsuDifficultyHitObject)current.Previous(1);
var lastObj2 = (OsuDifficultyHitObject)current.Previous(2);
if (currObj.Angle.IsNull() || lastObj0.Angle.IsNull() || lastObj1.Angle.IsNull() || lastObj2.Angle.IsNull())
return 0;
// Punishment will be reduced if velocity is changing
double velocityChangeFactor = getVelocityChangeFactor(currObj, lastObj0);
velocityChangeFactor = 1 - Math.Pow(velocityChangeFactor, 2);
double a1 = currObj.Angle.Value / Math.PI;
double a2 = lastObj0.Angle.Value / Math.PI;
double a3 = lastObj1.Angle.Value / Math.PI;
double a4 = lastObj2.Angle.Value / Math.PI;
// - 4 same sharp angles in a row: (0.3 0.3 0.3 0.3) -> max punishment
// Normalized difference
double angleDifference1 = Math.Abs(a1 - a2);
double angleDifference2 = Math.Abs(a1 - a3);
double angleDifference3 = Math.Abs(a1 - a4);
// Will be close to 1 if angleDifference1 and angleDifference2 was both close to 0
double sameAnglePunishment = Math.Pow((1 - angleDifference1) * (1 - angleDifference2) * (1 - angleDifference3), 3);
// Starting from 60 degrees - reduce same angle punishment
double angleSharpnessFactor = Math.Max(0, a1 - 1.0 / 3);
angleSharpnessFactor = 1 - angleSharpnessFactor;
sameAnglePunishment *= angleSharpnessFactor;
sameAnglePunishment *= velocityChangeFactor;
sameAnglePunishment *= 0.75;
// - Alternating angles with 0: (0.3 0 0.3 0) or (0 0.3 0 0.3) -> max punishment, (0.3 0 0.1 0) -> some punishment
double alternateWithZeroAnglePunishment = Math.Max(
getAlternateWithZeroAnglePunishment(a1, a2, a3, a4),
getAlternateWithZeroAnglePunishment(a2, a1, a4, a3));
alternateWithZeroAnglePunishment *= velocityChangeFactor;
return Math.Min(1, sameAnglePunishment + alternateWithZeroAnglePunishment);
}
private static double getVelocityChangeFactor(OsuDifficultyHitObject osuCurrObj, OsuDifficultyHitObject osuLastObj)
{
double currVelocity = osuCurrObj.LazyJumpDistance / osuCurrObj.StrainTime;
double prevVelocity = osuLastObj.LazyJumpDistance / osuLastObj.StrainTime;
double velocityChangeFactor = 0;
// https://www.desmos.com/calculator/kqxmqc8pkg
if (currVelocity > 0 || prevVelocity > 0)
{
double velocityChange = Math.Max(0,
Math.Min(
Math.Abs(prevVelocity - currVelocity) - 0.5 * Math.Min(currVelocity, prevVelocity),
Math.Max(((OsuHitObject)osuCurrObj.BaseObject).Radius / Math.Max(osuCurrObj.StrainTime, osuLastObj.StrainTime), Math.Min(currVelocity, prevVelocity))
)); // Stealed from xexxar
velocityChangeFactor = velocityChange / Math.Max(currVelocity, prevVelocity); // maxiumum is 0.4
velocityChangeFactor /= 0.4;
}
return velocityChangeFactor;
}
private static double getAlternateWithZeroAnglePunishment(double a1, double a2, double a3, double a4)
{
// We assume that a1 and a3 are 0
double zeroFactor = Math.Pow((1 - a1) * (1 - a3), 8);
zeroFactor *= Math.Pow(1 - Math.Abs(a1 - a3), 2);
double angleSimilarityFactor = 1 - Math.Abs(a2 - a4);
double angleSharpnessFactor = Math.Min(1 - Math.Max(0, a2 - 1.0 / 3), 1 - Math.Max(0, a4 - 1.0 / 3));
return zeroFactor * angleSimilarityFactor * angleSharpnessFactor;
}
public static double EvaluateLowDensityBonusOf(DifficultyHitObject current)
{
//var currObj = (OsuDifficultyHitObject)current;
//// Density = 2 in general means 3 notes on screen (it's not including current note)
//double density = CalculateDenstityOf(currObj);
//// We are considering density = 1.5 as starting point, 1.0 is noticably uncomfy and 0.5 is severely uncomfy
//double bonus = 1.5 - density;
//if (bonus <= 0) return 0;
//return Math.Pow(bonus, 2);
return 0;
}
// https://www.desmos.com/calculator/hbj7swzlth
private static double highArCurve(double preempt)
{
double value = Math.Pow(3, 3 - 0.01 * preempt); // 1 for 300ms, 0.25 for 400ms, 0.0625 for 500ms
value = softmin(value, 2, 1.7); // use softmin to achieve full-memory cap, 2 times more than AR11 (300ms)
return value;
}
// We are using mutiply and divide instead of add and subtract, so values won't be negative
// https://www.desmos.com/calculator/fv5xerwpd2
private static double softmin(double a, double b, double power = Math.E) => a * b / Math.Log(Math.Pow(power, a) + Math.Pow(power, b), power);
}
}

View File

@ -22,11 +22,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty
{
public class OsuDifficultyCalculator : DifficultyCalculator
{
private const double difficulty_multiplier = 0.067;
public const double DIFFICULTY_MULTIPLIER = 0.067;
public const double SUM_POWER = 1.1;
public const double FL_SUM_POWER = 1.6;
public override int Version => 20220902;
public static double SumPower => 1.1;
public static double FLSumPower => 1.6;
public OsuDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap)
: base(ruleset, beatmap)
@ -38,16 +37,16 @@ namespace osu.Game.Rulesets.Osu.Difficulty
if (beatmap.HitObjects.Count == 0)
return new OsuDifficultyAttributes { Mods = mods };
double aimRating = Math.Sqrt(skills[0].DifficultyValue()) * difficulty_multiplier;
double aimRatingNoSliders = Math.Sqrt(skills[1].DifficultyValue()) * difficulty_multiplier;
double speedRating = Math.Sqrt(skills[2].DifficultyValue()) * difficulty_multiplier;
double aimRating = Math.Sqrt(skills[0].DifficultyValue()) * DIFFICULTY_MULTIPLIER;
double aimRatingNoSliders = Math.Sqrt(skills[1].DifficultyValue()) * DIFFICULTY_MULTIPLIER;
double speedRating = Math.Sqrt(skills[2].DifficultyValue()) * DIFFICULTY_MULTIPLIER;
double speedNotes = ((Speed)skills[2]).RelevantNoteCount();
double flashlightRating = Math.Sqrt(skills[3].DifficultyValue()) * difficulty_multiplier;
double flashlightRating = Math.Sqrt(skills[3].DifficultyValue()) * DIFFICULTY_MULTIPLIER;
double readingLowARRating = Math.Sqrt(skills[4].DifficultyValue()) * difficulty_multiplier;
double readingHighARRating = Math.Sqrt(skills[5].DifficultyValue()) * difficulty_multiplier;
double readingLowARRating = Math.Sqrt(skills[4].DifficultyValue()) * DIFFICULTY_MULTIPLIER;
double readingHighARRating = Math.Sqrt(skills[5].DifficultyValue()) * DIFFICULTY_MULTIPLIER;
double readingSlidersRating = 0;
double hiddenRating = Math.Sqrt(skills[6].DifficultyValue()) * difficulty_multiplier;
double hiddenRating = Math.Sqrt(skills[6].DifficultyValue()) * DIFFICULTY_MULTIPLIER;
double sliderFactor = aimRating > 0 ? aimRatingNoSliders / aimRating : 1;
@ -64,8 +63,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty
flashlightRating *= 0.7;
}
double baseAimPerformance = Math.Pow(5 * Math.Max(1, aimRating / 0.0675) - 4, 3) / 100000;
double baseSpeedPerformance = Math.Pow(5 * Math.Max(1, speedRating / 0.0675) - 4, 3) / 100000;
double baseAimPerformance = OsuStrainSkill.DifficultyToPerformance(aimRating);
double baseSpeedPerformance = OsuStrainSkill.DifficultyToPerformance(speedRating);
// Cognition
double baseFlashlightPerformance = 0.0;
@ -73,10 +72,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty
baseFlashlightPerformance = Math.Pow(flashlightRating, 2.0) * 25.0;
double baseReadingLowARPerformance = Math.Pow(readingLowARRating, 2.5) * 17.0;
double baseReadingHighARPerformance = Math.Pow(5 * Math.Max(1, readingHighARRating / 0.0675) - 4, 3) / 100000;
double baseReadingARPerformance = Math.Pow(Math.Pow(baseReadingLowARPerformance, SumPower) + Math.Pow(baseReadingHighARPerformance, SumPower), 1.0 / SumPower);
double baseReadingHighARPerformance = OsuStrainSkill.DifficultyToPerformance(readingHighARRating);
double baseReadingARPerformance = Math.Pow(Math.Pow(baseReadingLowARPerformance, SUM_POWER) + Math.Pow(baseReadingHighARPerformance, SUM_POWER), 1.0 / SUM_POWER);
double baseFlashlightARPerformance = Math.Pow(Math.Pow(baseFlashlightPerformance, FLSumPower) + Math.Pow(baseReadingARPerformance, FLSumPower), 1.0 / FLSumPower);
double baseFlashlightARPerformance = Math.Pow(Math.Pow(baseFlashlightPerformance, FL_SUM_POWER) + Math.Pow(baseReadingARPerformance, FL_SUM_POWER), 1.0 / FL_SUM_POWER);
double baseReadingHiddenPerformance = 0;
if (mods.Any(h => h is OsuModHidden))
@ -86,6 +85,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
double baseReadingNonARPerformance = baseReadingHiddenPerformance + baseReadingSliderPerformance;
double preempt = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.ApproachRate, 1800, 1200, 450) / clockRate;
double drainRate = beatmap.Difficulty.DrainRate;
int maxCombo = beatmap.GetMaxCombo();
@ -94,16 +94,16 @@ namespace osu.Game.Rulesets.Osu.Difficulty
int spinnerCount = beatmap.HitObjects.Count(h => h is Spinner);
// Limit cognition by full memorisation difficulty
double cognitionPerformance = Math.Pow(Math.Pow(baseFlashlightARPerformance, SumPower) + Math.Pow(baseReadingNonARPerformance, SumPower), 1.0 / SumPower);
double mechanicalPerformance = Math.Pow(Math.Pow(baseAimPerformance, SumPower) + Math.Pow(baseSpeedPerformance, SumPower), 1.0 / SumPower);
double cognitionPerformance = Math.Pow(Math.Pow(baseFlashlightARPerformance, SUM_POWER) + Math.Pow(baseReadingNonARPerformance, SUM_POWER), 1.0 / SUM_POWER);
double mechanicalPerformance = Math.Pow(Math.Pow(baseAimPerformance, SUM_POWER) + Math.Pow(baseSpeedPerformance, SUM_POWER), 1.0 / SUM_POWER);
double potentialFlashlightPerformance = OsuPerformanceCalculator.ComputePerfectFlashlightValue(flashlightRating, hitCirclesCount + sliderCount);
cognitionPerformance = OsuPerformanceCalculator.AdjustCognitionPerformance(cognitionPerformance, mechanicalPerformance, potentialFlashlightPerformance);
double basePerformance =
Math.Pow(
Math.Pow(mechanicalPerformance, SumPower) +
Math.Pow(cognitionPerformance, SumPower)
, 1.0 / SumPower
Math.Pow(mechanicalPerformance, SUM_POWER) +
Math.Pow(cognitionPerformance, SUM_POWER)
, 1.0 / SUM_POWER
);
double starRating = basePerformance > 0.00001
@ -115,6 +115,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty
double hitWindowGreat = hitWindows.WindowFor(HitResult.Great) / clockRate;
//var test = ((ReadingHighAR)skills[5]).GetAimSpeed();
OsuDifficultyAttributes attributes = new OsuDifficultyAttributes
{
StarRating = starRating,
@ -128,7 +130,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
HiddenDifficulty = hiddenRating,
FlashlightDifficulty = flashlightRating,
SliderFactor = sliderFactor,
ApproachRate = preempt > 1200 ? (1800 - preempt) / 120 : (1200 - preempt) / 150 + 5,
ApproachRate = IBeatmapDifficultyInfo.InverseDifficultyRange(preempt, 1800, 1200, 450),
OverallDifficulty = (80 - hitWindowGreat) / 6,
DrainRate = drainRate,
MaxCombo = maxCombo,

View File

@ -4,8 +4,8 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.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;
@ -43,6 +43,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
effectiveMissCount = calculateEffectiveMissCount(osuAttributes);
double multiplier = PERFORMANCE_BASE_MULTIPLIER;
double power = OsuDifficultyCalculator.SUM_POWER;
if (score.Mods.Any(m => m is OsuModNoFail))
multiplier *= Math.Max(0.90, 1.0 - 0.02 * effectiveMissCount);
@ -62,8 +63,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty
effectiveMissCount = Math.Min(effectiveMissCount + countOk * okMultiplier + countMeh * mehMultiplier, totalHits);
}
double power = OsuDifficultyCalculator.SumPower;
double aimValue = computeAimValue(score, osuAttributes);
double speedValue = computeSpeedValue(score, osuAttributes);
double mechanicalValue = Math.Pow(Math.Pow(aimValue, power) + Math.Pow(speedValue, power), 1.0 / power);
@ -75,12 +74,22 @@ namespace osu.Game.Rulesets.Osu.Difficulty
double flashlightValue = potentialFlashlightValue;
if (!score.Mods.Any(h => h is OsuModFlashlight))
flashlightValue = 0.0;
double readingARValue = computeReadingARValue(score, osuAttributes);
double lowARValue = computeReadingLowARValue(score, osuAttributes);
double readingHDValue = computeReadingHiddenValue(score, osuAttributes);
double readingSlidersValue = 0;
double highARValue = computeReadingHighARValue(score, osuAttributes);
double readingARValue = Math.Pow(
Math.Pow(lowARValue, power) +
Math.Pow(highARValue, power), 1.0 / power);
// Reduce AR reading bonus if FL is present
double flPower = OsuDifficultyCalculator.FLSumPower;
double flPower = OsuDifficultyCalculator.FL_SUM_POWER;
double flashlightARValue = Math.Pow(Math.Pow(flashlightValue, flPower) + Math.Pow(readingARValue, flPower), 1.0 / flPower);
double readingNonARValue = computeReadingNonARValue(score, osuAttributes);
double readingNonARValue = readingHDValue + readingSlidersValue;
double cognitionValue = Math.Pow(Math.Pow(flashlightARValue, power) + Math.Pow(readingNonARValue, power), 1.0 / power);
cognitionValue = AdjustCognitionPerformance(cognitionValue, mechanicalValue, potentialFlashlightValue);
@ -106,7 +115,7 @@ 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);
@ -143,7 +152,7 @@ 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);
@ -245,25 +254,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty
return flashlightValue;
}
private double computeReadingARValue(ScoreInfo score, OsuDifficultyAttributes attributes)
{
//double readingARValue = Math.Max(computeReadingLowARValue(score, attributes), computeReadingHighARValue(score, attributes));
double power = OsuDifficultyCalculator.SumPower;
double readingValue = Math.Pow(
Math.Pow(computeReadingLowARValue(score, attributes), power) +
Math.Pow(computeReadingHighARValue(score, attributes), power), 1.0 / power);
return readingValue;
}
private double computeReadingNonARValue(ScoreInfo score, OsuDifficultyAttributes attributes)
{
double readingHDValue = computeReadingHiddenValue(score, attributes);
double readingSlidersValue = 0;
return readingHDValue + readingSlidersValue;
}
private double computeReadingLowARValue(ScoreInfo score, OsuDifficultyAttributes attributes)
{
double rawReading = attributes.ReadingDifficultyLowAR;
@ -289,8 +279,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
private double computeReadingHighARValue(ScoreInfo score, OsuDifficultyAttributes attributes)
{
// Copied from aim
double highARValue = Math.Pow(5.0 * Math.Max(1.0, attributes.ReadingDifficultyHighAR / 0.0675) - 4.0, 3.0) / 100000.0;
double highARValue = OsuStrainSkill.DifficultyToPerformance(attributes.ReadingDifficultyHighAR);
// High AR should have length bonus, even more agressive than normal aim
double lengthBonus = 0.95 + 0.4 * Math.Min(1.0, totalHits / 2000.0) +
@ -303,11 +292,48 @@ namespace osu.Game.Rulesets.Osu.Difficulty
highARValue *= getComboScalingFactor(attributes);
highARValue *= accuracy * accuracy;
// It is important to consider accuracy difficulty when scaling with accuracy.
highARValue *= 0.98 + Math.Pow(attributes.OverallDifficulty, 2) / 2500;
// Approximate how much of high AR difficulty is aim
double aimPerformance = OsuStrainSkill.DifficultyToPerformance(attributes.AimDifficulty);
double speedPerformance = OsuStrainSkill.DifficultyToPerformance(attributes.SpeedDifficulty);
return highARValue;
double aimRatio = aimPerformance / (aimPerformance + speedPerformance);
// Aim part calculation
double aimPartValue = highARValue * aimRatio;
{
// We assume 15% of sliders in a map are difficult since there's no way to tell from the performance calculator.
double estimateDifficultSliders = attributes.SliderCount * 0.15;
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;
aimPartValue *= sliderNerfFactor;
}
aimPartValue *= accuracy;
// It is important to consider accuracy difficulty when scaling with accuracy.
aimPartValue *= 0.98 + Math.Pow(attributes.OverallDifficulty, 2) / 2500;
}
// Speed part calculation
double speedPartValue = highARValue * (1 - aimRatio);
{
// Calculate accuracy assuming the worst case scenario
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 relevantAccuracy = attributes.SpeedNoteCount == 0 ? 0 : (relevantCountGreat * 6.0 + relevantCountOk * 2.0 + relevantCountMeh) / (attributes.SpeedNoteCount * 6.0);
// Scale the speed value with accuracy and OD.
speedPartValue *= (0.95 + Math.Pow(attributes.OverallDifficulty, 2) / 750) * Math.Pow((accuracy + relevantAccuracy) / 2.0, (14.5 - Math.Max(attributes.OverallDifficulty, 8)) / 2);
// Scale the speed value with # of 50s to punish doubletapping.
speedPartValue *= Math.Pow(0.99, countMeh < totalHits / 500.0 ? 0 : countMeh - totalHits / 500.0);
}
return aimPartValue + speedPartValue;
}
private double computeReadingHiddenValue(ScoreInfo score, OsuDifficultyAttributes attributes)
@ -316,6 +342,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
return 0.0;
double rawReading = attributes.HiddenDifficulty;
//double readingValue = Math.Pow(rawReading, 2.0) * 25.0;
double readingValue = Math.Pow(rawReading, 2.0) * 25.0;
// Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses.

View File

@ -95,11 +95,25 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
/// </summary>
public IList<OverlapObject> OverlapObjects { get; private set; }
/// <summary>
/// Time in ms between appearence of this <see cref="OsuDifficultyHitObject"/> and moment to click on it.
/// </summary>
public readonly double Preempt;
/// <summary>
/// Preempt of follow line for this <see cref="OsuDifficultyHitObject"/> adjusted by clockrate.
/// Will be equal to 0 if object is New Combo.
/// </summary>
public readonly double FollowLineTime;
/// <summary>
/// Playback rate of beatmap.
/// Will be equal 1.5 on DT and 0.75 on HT.
/// </summary>
public readonly double ClockRate;
private readonly OsuHitObject? lastLastObject;
private readonly OsuHitObject lastObject;
public readonly double Preempt;
public readonly double FollowLineTime;
public readonly double ClockRate;
public OsuDifficultyHitObject(HitObject hitObject, HitObject lastObject, HitObject? lastLastObject, double clockRate, List<DifficultyHitObject> objects, int index)
: base(hitObject, lastObject, clockRate, objects, index)

View File

@ -7,6 +7,7 @@ using osu.Game.Rulesets.Difficulty.Skills;
using osu.Game.Rulesets.Mods;
using System.Linq;
using osu.Framework.Utils;
using System.Xml.Linq;
namespace osu.Game.Rulesets.Osu.Difficulty.Skills
{
@ -67,5 +68,15 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
return difficulty * DifficultyMultiplier;
}
/// <summary>
/// Converts difficulty value from <see cref="OsuDifficultyAttributes"/> to base performance.
/// </summary>
public static double DifficultyToPerformance(double difficulty) => Math.Pow(5.0 * Math.Max(1.0, difficulty / 0.0675) - 4.0, 3.0) / 100000.0;
/// <summary>
/// Converts base performance to <see cref="OsuDifficultyAttributes"/> difficulty value.s
/// </summary>
public static double PerformanceToDifficulty(double performance) => (Math.Pow(100000.0 * performance, 1.0 / 3.0) + 4.0) / 5.0 * 0.0675;
}
}

View File

@ -9,7 +9,6 @@ using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Difficulty.Skills;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Difficulty.Evaluators;
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
namespace osu.Game.Rulesets.Osu.Difficulty.Skills
{
@ -75,65 +74,15 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
}
}
public class ReadingHighAR : GraphSkill
public class ReadingHidden : OsuStrainSkill
{
public ReadingHighAR(Mod[] mods)
: base(mods)
{
aimComponent = new HighARAimComponent(mods);
speedComponent = new HighARSpeedComponent(mods);
}
private HighARAimComponent aimComponent;
private HighARSpeedComponent speedComponent;
private readonly List<double> difficulties = new List<double>();
public override void Process(DifficultyHitObject current)
{
aimComponent.Process(current);
speedComponent.Process(current);
double power = OsuDifficultyCalculator.SumPower;
double mergedDifficulty = Math.Pow(
Math.Pow(aimComponent.CurrentSectionPeak, power) +
Math.Pow(speedComponent.CurrentSectionPeak, power), 1.0 / power);
difficulties.Add(mergedDifficulty);
if (current.Index == 0)
CurrentSectionEnd = Math.Ceiling(current.StartTime / SectionLength) * SectionLength;
while (current.StartTime > CurrentSectionEnd)
{
StrainPeaks.Add(CurrentSectionPeak);
CurrentSectionPeak = 0;
CurrentSectionEnd += SectionLength;
}
CurrentSectionPeak = Math.Max(mergedDifficulty, CurrentSectionPeak);
}
public override double DifficultyValue()
{
double power = OsuDifficultyCalculator.SumPower;
return Math.Pow(
Math.Pow(aimComponent.DifficultyValue(), power) +
Math.Pow(speedComponent.DifficultyValue(), power), 1.0 / power);
}
}
public class HighARAimComponent : OsuStrainSkill
{
public HighARAimComponent(Mod[] mods)
public ReadingHidden(Mod[] mods)
: base(mods)
{
}
private double currentStrain;
// private double currentRhythm;
//private double skillMultiplier => 13;
private double skillMultiplier => 14;
private double skillMultiplier => 5;
private double strainDecayBase => 0.15;
private double strainDecay(double ms) => Math.Pow(strainDecayBase, ms / 1000);
@ -144,105 +93,14 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
{
currentStrain *= strainDecay(current.DeltaTime);
double aimDifficulty = AimEvaluator.EvaluateDifficultyOf(current, true, ((OsuDifficultyHitObject)current).Preempt);
aimDifficulty *= ReadingEvaluator.EvaluateHighARDifficultyOf(current, true);
aimDifficulty *= skillMultiplier;
// We're not using slider aim because we assuming that HD doesn't makes sliders harder (what is not true, but we will ignore this for now)
double hiddenDifficulty = AimEvaluator.EvaluateDifficultyOf(current, false);
hiddenDifficulty *= ReadingHiddenEvaluator.EvaluateDifficultyOf(current);
hiddenDifficulty *= skillMultiplier;
double totalStrain = currentStrain;
currentStrain += aimDifficulty;
currentStrain += hiddenDifficulty;
// Warning: this line is unstable, so increasing amount of objects can decrease pp
totalStrain += aimDifficulty * (1 + ReadingEvaluator.EvaluateLowDensityBonusOf(current));
//Console.WriteLine($"{current.StartTime} - {ReadingEvaluator.EvaluateLowDensityBonusOf(current)}");
return totalStrain;
}
}
public class HighARSpeedComponent : OsuStrainSkill
{
private double skillMultiplier => 675;
private double strainDecayBase => 0.3;
private double currentStrain;
private double currentRhythm;
public HighARSpeedComponent(Mod[] mods)
: base(mods)
{
}
private double strainDecay(double ms) => Math.Pow(strainDecayBase, ms / 1000);
protected override double CalculateInitialStrain(double time, DifficultyHitObject current) => (currentStrain * currentRhythm) * strainDecay(time - current.Previous(0).StartTime);
protected override double StrainValueAt(DifficultyHitObject current)
{
OsuDifficultyHitObject currODHO = (OsuDifficultyHitObject)current;
currentStrain *= strainDecay(currODHO.StrainTime);
double speedDifficulty = SpeedEvaluator.EvaluateDifficultyOf(current) * skillMultiplier;
speedDifficulty *= ReadingEvaluator.EvaluateHighARDifficultyOf(current, false);
currentStrain += speedDifficulty;
currentRhythm = currODHO.RhythmDifficulty;
// currentRhythm *= currentRhythm; // Squaring is broken cuz rhythm is broken ((((
double totalStrain = currentStrain * currentRhythm;
return totalStrain;
}
}
public class ReadingHidden : GraphSkill
{
public ReadingHidden(Mod[] mods)
: base(mods)
{
}
private readonly List<double> difficulties = new List<double>();
private double skillMultiplier => 2.3;
public override void Process(DifficultyHitObject current)
{
double currentDifficulty = ReadingEvaluator.EvaluateHiddenDifficultyOf(current) * skillMultiplier;
difficulties.Add(currentDifficulty);
if (current.Index == 0)
CurrentSectionEnd = Math.Ceiling(current.StartTime / SectionLength) * SectionLength;
while (current.StartTime > CurrentSectionEnd)
{
StrainPeaks.Add(CurrentSectionPeak);
CurrentSectionPeak = 0;
CurrentSectionEnd += SectionLength;
}
CurrentSectionPeak = Math.Max(currentDifficulty, CurrentSectionPeak);
}
public override double DifficultyValue()
{
double difficulty = 0;
// Sections with 0 difficulty are excluded to avoid worst-case time complexity of the following sort (e.g. /b/2351871).
// These sections will not contribute to the difficulty.
var peaks = difficulties.Where(p => p > 0);
List<double> values = peaks.OrderByDescending(d => d).ToList();
// Difficulty is the weighted sum of the highest strains from every section.
// We're sorting from highest to lowest strain.
for (int i = 0; i < values.Count; i++)
{
difficulty += values[i] / (i + 1);
}
return difficulty;
return currentStrain;
}
}
}

View File

@ -0,0 +1,146 @@
// 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.Collections.Generic;
using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Difficulty.Skills;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Difficulty.Evaluators;
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
namespace osu.Game.Rulesets.Osu.Difficulty.Skills
{
public class ReadingHighAR : GraphSkill
{
public ReadingHighAR(Mod[] mods)
: base(mods)
{
aimComponent = new HighARAimComponent(mods);
speedComponent = new HighARSpeedComponent(mods);
aimComponentNoAdjust = new HighARAimComponent(mods, false);
}
private HighARAimComponent aimComponent;
private HighARAimComponent aimComponentNoAdjust;
private HighARSpeedComponent speedComponent;
private readonly List<double> difficulties = new List<double>();
public override void Process(DifficultyHitObject current)
{
aimComponent.Process(current);
speedComponent.Process(current);
aimComponentNoAdjust.Process(current);
double power = OsuDifficultyCalculator.SUM_POWER;
double mergedDifficulty = Math.Pow(
Math.Pow(aimComponent.CurrentSectionPeak, power) +
Math.Pow(speedComponent.CurrentSectionPeak, power), 1.0 / power);
difficulties.Add(mergedDifficulty);
if (current.Index == 0)
CurrentSectionEnd = Math.Ceiling(current.StartTime / SectionLength) * SectionLength;
while (current.StartTime > CurrentSectionEnd)
{
StrainPeaks.Add(CurrentSectionPeak);
CurrentSectionPeak = 0;
CurrentSectionEnd += SectionLength;
}
CurrentSectionPeak = Math.Max(mergedDifficulty, CurrentSectionPeak);
}
public override double DifficultyValue()
{
Console.WriteLine($"Degree of High AR Complexity = {aimComponent.DifficultyValue() / aimComponentNoAdjust.DifficultyValue():0.##}");
// Simulating summing
double aimValue = Math.Sqrt(aimComponent.DifficultyValue()) * OsuDifficultyCalculator.DIFFICULTY_MULTIPLIER;
double speedValue = Math.Sqrt(speedComponent.DifficultyValue()) * OsuDifficultyCalculator.DIFFICULTY_MULTIPLIER;
double aimPerformance = OsuStrainSkill.DifficultyToPerformance(aimValue);
double speedPerformance = OsuStrainSkill.DifficultyToPerformance(speedValue);
double power = OsuDifficultyCalculator.SUM_POWER;
double totalPerformance = Math.Pow(Math.Pow(aimPerformance, power) + Math.Pow(speedPerformance, power), 1.0 / power);
double adjustedDifficulty = OsuStrainSkill.PerformanceToDifficulty(totalPerformance);
return Math.Pow(adjustedDifficulty / OsuDifficultyCalculator.DIFFICULTY_MULTIPLIER, 2.0);
}
}
public class HighARAimComponent : OsuStrainSkill
{
public HighARAimComponent(Mod[] mods, bool adjustHighAR = true)
: base(mods)
{
this.adjustHighAR = adjustHighAR;
}
private bool adjustHighAR;
private double currentStrain;
private double skillMultiplier => 19;
private double strainDecayBase => 0.15;
private double strainDecay(double ms) => Math.Pow(strainDecayBase, ms / 1000);
protected override double CalculateInitialStrain(double time, DifficultyHitObject current) => currentStrain * strainDecay(time - current.Previous(0).StartTime);
protected override double StrainValueAt(DifficultyHitObject current)
{
currentStrain *= strainDecay(current.DeltaTime);
double aimDifficulty = AimEvaluator.EvaluateDifficultyOf(current, true);
aimDifficulty *= ReadingHighAREvaluator.EvaluateDifficultyOf(current, adjustHighAR);
aimDifficulty *= skillMultiplier;
double totalStrain = currentStrain;
currentStrain += aimDifficulty;
totalStrain += aimDifficulty;
// Console.WriteLine($"{current.BaseObject.StartTime},{aimDifficulty:0.#}");
return totalStrain;
}
}
public class HighARSpeedComponent : OsuStrainSkill
{
private double skillMultiplier => 850;
private double strainDecayBase => 0.3;
private double currentStrain;
private double currentRhythm;
public HighARSpeedComponent(Mod[] mods)
: base(mods)
{
}
private double strainDecay(double ms) => Math.Pow(strainDecayBase, ms / 1000);
protected override double CalculateInitialStrain(double time, DifficultyHitObject current) => (currentStrain * currentRhythm) * strainDecay(time - current.Previous(0).StartTime);
protected override double StrainValueAt(DifficultyHitObject current)
{
OsuDifficultyHitObject currODHO = (OsuDifficultyHitObject)current;
currentStrain *= strainDecay(currODHO.StrainTime);
double speedDifficulty = SpeedEvaluator.EvaluateDifficultyOf(current) * skillMultiplier;
speedDifficulty *= ReadingHighAREvaluator.EvaluateDifficultyOf(current, false);
currentStrain += speedDifficulty;
currentRhythm = currODHO.RhythmDifficulty;
double totalStrain = currentStrain * currentRhythm;
return totalStrain;
}
}
}

View File

@ -98,7 +98,7 @@ namespace osu.Game.Rulesets.Osu.Objects
set
{
repeatCount = value;
endPositionCache.Invalidate();
updateNestedPositions();
}
}
@ -165,7 +165,7 @@ namespace osu.Game.Rulesets.Osu.Objects
public Slider()
{
SamplesBindable.CollectionChanged += (_, _) => UpdateNestedSamples();
Path.Version.ValueChanged += _ => endPositionCache.Invalidate();
Path.Version.ValueChanged += _ => updateNestedPositions();
}
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty)