2022-05-28 20:28:04 +08:00
// 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.
2022-05-28 20:29:09 +08:00
using System ;
2024-07-15 17:45:31 +08:00
using System.Collections.Generic ;
2024-09-19 20:52:55 +08:00
using System.Linq ;
2022-05-28 20:29:09 +08:00
using osu.Game.Rulesets.Difficulty.Preprocessing ;
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing ;
using osu.Game.Rulesets.Osu.Objects ;
2022-05-28 20:28:04 +08:00
namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
{
2022-05-28 20:29:09 +08:00
public static class RhythmEvaluator
2022-05-28 20:28:04 +08:00
{
2024-09-19 18:53:18 +08:00
private const int history_time_max = 4 * 1000 ; // 4 seconds
2024-09-24 20:57:31 +08:00
private const int history_objects_max = 32 ;
private const double rhythm_overall_multiplier = 0.9 ;
private const double rhythm_ratio_multiplier = 11.5 ;
2022-05-28 20:29:09 +08:00
/// <summary>
/// Calculates a rhythm multiplier for the difficulty of the tap associated with historic data of the current <see cref="OsuDifficultyHitObject"/>.
/// </summary>
2024-09-16 03:49:36 +08:00
public static double EvaluateDifficultyOf ( DifficultyHitObject current )
2022-05-28 20:29:09 +08:00
{
if ( current . BaseObject is Spinner )
return 0 ;
double rhythmComplexitySum = 0 ;
2024-07-15 17:45:31 +08:00
2024-08-28 02:48:15 +08:00
double deltaDifferenceEpsilon = ( ( OsuDifficultyHitObject ) current ) . HitWindowGreat * 0.3 ;
var island = new Island ( deltaDifferenceEpsilon ) ;
var previousIsland = new Island ( deltaDifferenceEpsilon ) ;
2024-09-19 20:52:55 +08:00
// we can't use dictionary here because we need to compare island with a tolerance
// which is impossible to pass into the hash comparer
var islandCounts = new List < ( Island Island , int Count ) > ( ) ;
2024-07-15 17:45:31 +08:00
2022-05-28 20:29:09 +08:00
double startRatio = 0 ; // store the ratio of the current start of an island to buff for tighter rhythms
bool firstDeltaSwitch = false ;
2024-09-16 03:49:36 +08:00
int historicalNoteCount = Math . Min ( current . Index , history_objects_max ) ;
2022-05-28 20:29:09 +08:00
int rhythmStart = 0 ;
2024-09-16 03:49:36 +08:00
while ( rhythmStart < historicalNoteCount - 2 & & current . StartTime - current . Previous ( rhythmStart ) . StartTime < history_time_max )
2022-05-28 20:29:09 +08:00
rhythmStart + + ;
2024-05-19 23:26:51 +08:00
OsuDifficultyHitObject prevObj = ( OsuDifficultyHitObject ) current . Previous ( rhythmStart ) ;
OsuDifficultyHitObject lastObj = ( OsuDifficultyHitObject ) current . Previous ( rhythmStart + 1 ) ;
2024-09-15 04:49:52 +08:00
// we go from the furthest object back to the current one
2022-05-28 20:29:09 +08:00
for ( int i = rhythmStart ; i > 0 ; i - - )
{
OsuDifficultyHitObject currObj = ( OsuDifficultyHitObject ) current . Previous ( i - 1 ) ;
2024-09-19 18:53:18 +08:00
// scales note 0 to 1 from history to now
double timeDecay = ( history_time_max - ( current . StartTime - currObj . StartTime ) ) / history_time_max ;
double noteDecay = ( double ) ( historicalNoteCount - i ) / historicalNoteCount ;
2022-05-28 20:29:09 +08:00
2024-09-19 18:53:18 +08:00
double currHistoricalDecay = Math . Min ( noteDecay , timeDecay ) ; // either we're limited by time or limited by object count.
2022-05-28 20:29:09 +08:00
double currDelta = currObj . StrainTime ;
double prevDelta = prevObj . StrainTime ;
double lastDelta = lastObj . StrainTime ;
2024-07-15 17:45:31 +08:00
2024-09-15 04:12:41 +08:00
// calculate how much current delta difference deserves a rhythm bonus
// this function is meant to reduce rhythm bonus for deltas that are multiples of each other (i.e 100 and 200)
double deltaDifferenceRatio = Math . Min ( prevDelta , currDelta ) / Math . Max ( prevDelta , currDelta ) ;
2024-09-16 04:04:46 +08:00
double currRatio = 1.0 + rhythm_ratio_multiplier * Math . Min ( 0.5 , Math . Pow ( Math . Sin ( Math . PI / deltaDifferenceRatio ) , 2 ) ) ;
2022-05-28 20:29:09 +08:00
2024-09-19 07:38:01 +08:00
// reduce ratio bonus if delta difference is too big
double fraction = Math . Max ( prevDelta / currDelta , currDelta / prevDelta ) ;
double fractionMultiplier = Math . Clamp ( 2.0 - fraction / 8.0 , 0.0 , 1.0 ) ;
2024-08-27 18:50:08 +08:00
double windowPenalty = Math . Min ( 1 , Math . Max ( 0 , Math . Abs ( prevDelta - currDelta ) - deltaDifferenceEpsilon ) / deltaDifferenceEpsilon ) ;
2022-05-28 20:29:09 +08:00
2024-09-19 07:38:01 +08:00
double effectiveRatio = windowPenalty * currRatio * fractionMultiplier ;
2022-05-28 20:29:09 +08:00
if ( firstDeltaSwitch )
{
2024-09-15 04:12:41 +08:00
if ( Math . Abs ( prevDelta - currDelta ) < deltaDifferenceEpsilon )
2022-05-28 20:29:09 +08:00
{
2024-08-22 18:59:13 +08:00
// island is still progressing
2024-08-28 02:48:15 +08:00
island . AddDelta ( ( int ) currDelta ) ;
2022-05-28 20:29:09 +08:00
}
else
{
2024-09-24 20:57:31 +08:00
// bpm change is into slider, this is easy acc window
if ( currObj . BaseObject is Slider )
effectiveRatio * = 0.125 ;
// bpm change was from a slider, this is easier typically than circle -> circle
// unintentional side effect is that bursts with kicksliders at the ends might have lower difficulty than bursts without sliders
if ( prevObj . BaseObject is Slider )
effectiveRatio * = 0.15 ;
2024-09-15 01:03:01 +08:00
// repeated island polarity (2 -> 4, 3 -> 5)
2024-08-28 02:48:15 +08:00
if ( island . IsSimilarPolarity ( previousIsland ) )
2024-08-24 07:37:58 +08:00
effectiveRatio * = 0.3 ;
2022-05-28 20:29:09 +08:00
2024-08-24 07:37:58 +08:00
// previous increase happened a note ago, 1/1->1/2-1/4, dont want to buff this.
if ( lastDelta > prevDelta + deltaDifferenceEpsilon & & prevDelta > currDelta + deltaDifferenceEpsilon )
2022-05-28 20:29:09 +08:00
effectiveRatio * = 0.125 ;
2024-09-15 01:03:01 +08:00
// singletaps are easier to control
2024-09-15 04:12:41 +08:00
if ( island . DeltaCount = = 1 )
2024-09-15 01:03:01 +08:00
effectiveRatio * = 0.7 ;
2024-09-19 20:52:55 +08:00
var islandCount = islandCounts . FirstOrDefault ( x = > x . Island . Equals ( island ) ) ;
if ( islandCount ! = default )
2024-07-15 17:45:31 +08:00
{
2024-09-19 22:15:54 +08:00
int countIndex = islandCounts . IndexOf ( islandCount ) ;
2024-08-28 02:48:15 +08:00
// only add island to island counts if they're going one after another
if ( previousIsland . Equals ( island ) )
2024-09-19 20:52:55 +08:00
islandCount . Count + + ;
2024-07-15 17:45:31 +08:00
// repeated island (ex: triplet -> triplet)
2024-09-24 20:57:31 +08:00
double power = logistic ( island . Delta , 2 , 0.165 , 10 ) ;
2024-09-19 20:52:55 +08:00
effectiveRatio * = Math . Min ( 3.0 / islandCount . Count , Math . Pow ( 1.0 / islandCount . Count , power ) ) ;
2024-09-19 22:15:54 +08:00
islandCounts [ countIndex ] = ( islandCount . Island , islandCount . Count ) ;
2024-09-19 20:52:55 +08:00
}
else
{
islandCounts . Add ( ( island , 1 ) ) ;
2024-07-15 17:45:31 +08:00
}
2024-08-24 07:37:58 +08:00
// scale down the difficulty if the object is doubletappable
double doubletapness = prevObj . GetDoubletapness ( ( OsuDifficultyHitObject ? ) prevObj . Next ( 0 ) ) ;
effectiveRatio * = 1 - doubletapness * 0.75 ;
2024-07-15 17:45:31 +08:00
rhythmComplexitySum + = Math . Sqrt ( effectiveRatio * startRatio ) * currHistoricalDecay ;
2022-05-28 20:29:09 +08:00
startRatio = effectiveRatio ;
2024-07-15 17:45:31 +08:00
previousIsland = island ;
2022-05-28 20:29:09 +08:00
2024-07-15 17:45:31 +08:00
if ( prevDelta + deltaDifferenceEpsilon < currDelta ) // we're slowing down, stop counting
2024-09-15 04:49:52 +08:00
firstDeltaSwitch = false ; // if we're speeding up, this stays true and we keep counting island size.
2022-05-28 20:29:09 +08:00
2024-07-15 17:45:31 +08:00
island = new Island ( ( int ) currDelta , deltaDifferenceEpsilon ) ;
2022-05-28 20:29:09 +08:00
}
}
2024-09-15 04:49:52 +08:00
else if ( prevDelta > currDelta + deltaDifferenceEpsilon ) // we're speeding up
2022-05-28 20:29:09 +08:00
{
// Begin counting island until we change speed again.
firstDeltaSwitch = true ;
2024-09-15 04:49:52 +08:00
2022-05-28 20:29:09 +08:00
startRatio = effectiveRatio ;
2024-07-15 17:45:31 +08:00
island = new Island ( ( int ) currDelta , deltaDifferenceEpsilon ) ;
2022-05-28 20:29:09 +08:00
}
2024-05-19 23:26:51 +08:00
lastObj = prevObj ;
prevObj = currObj ;
2022-05-28 20:29:09 +08:00
}
2024-09-16 04:04:46 +08:00
return Math . Sqrt ( 4 + rhythmComplexitySum * rhythm_overall_multiplier ) / 2.0 ; // produces multiplier that can be applied to strain. range [1, infinity) (not really though)
2022-05-28 20:29:09 +08:00
}
2024-07-15 17:45:31 +08:00
2024-07-16 01:54:25 +08:00
private static double logistic ( double x , double maxValue , double multiplier , double offset ) = > ( maxValue / ( 1 + Math . Pow ( Math . E , offset - ( multiplier * x ) ) ) ) ;
2024-09-19 18:53:18 +08:00
2024-09-19 20:52:55 +08:00
private class Island : IEquatable < Island >
2024-09-19 18:53:18 +08:00
{
private readonly double deltaDifferenceEpsilon ;
public Island ( double epsilon )
{
deltaDifferenceEpsilon = epsilon ;
}
public Island ( int delta , double epsilon )
{
deltaDifferenceEpsilon = epsilon ;
Delta = Math . Max ( delta , OsuDifficultyHitObject . MIN_DELTA_TIME ) ;
2024-09-19 20:52:55 +08:00
DeltaCount + + ;
2024-09-19 18:53:18 +08:00
}
public int Delta { get ; private set ; }
public int DeltaCount { get ; private set ; }
public void AddDelta ( int delta )
{
if ( Delta = = default )
Delta = Math . Max ( delta , OsuDifficultyHitObject . MIN_DELTA_TIME ) ;
DeltaCount + + ;
}
public bool IsSimilarPolarity ( Island other )
{
// consider islands to be of similar polarity only if they're having the same average delta (we don't want to consider 3 singletaps similar to a triple)
return DeltaCount % 2 = = other . DeltaCount % 2 & &
Math . Abs ( Delta - other . Delta ) < deltaDifferenceEpsilon ;
}
2024-09-19 20:52:55 +08:00
public bool Equals ( Island ? other )
2024-09-19 18:53:18 +08:00
{
2024-09-19 20:52:55 +08:00
if ( other = = null )
return false ;
2024-09-19 18:53:18 +08:00
return Math . Abs ( Delta - other . Delta ) < deltaDifferenceEpsilon & &
DeltaCount = = other . DeltaCount ;
}
2024-09-19 20:52:55 +08:00
public override string ToString ( )
2024-09-19 18:53:18 +08:00
{
2024-09-19 20:52:55 +08:00
return $"{Delta}x{DeltaCount}" ;
2024-09-19 18:53:18 +08:00
}
}
2022-05-28 20:28:04 +08:00
}
}