2023-06-23 00:37:25 +08:00
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
2022-05-28 20:28:04 +08:00
// 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 ;
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-07-15 17:45:31 +08:00
private readonly struct Island : IEquatable < Island >
{
public Island ( )
{
}
public Island ( int firstDelta , double epsilon )
{
AddDelta ( firstDelta , epsilon ) ;
}
public List < int > Deltas { get ; } = new List < int > ( ) ;
public void AddDelta ( int delta , double epsilon )
{
int existingDelta = Deltas . FirstOrDefault ( x = > Math . Abs ( x - delta ) > = epsilon ) ;
Deltas . Add ( existingDelta = = default ? delta : existingDelta ) ;
}
2024-07-19 13:13:50 +08:00
public double AverageDelta ( ) = > Deltas . Count > 0 ? Math . Max ( Deltas . Average ( ) , OsuDifficultyHitObject . MIN_DELTA_TIME ) : 0 ;
public bool IsSimilarPolarity ( Island other , double epsilon )
{
// 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 Math . Abs ( AverageDelta ( ) - other . AverageDelta ( ) ) < epsilon & &
Deltas . Count % 2 = = other . Deltas . Count % 2 ;
}
2024-07-15 17:45:31 +08:00
public override int GetHashCode ( )
{
// we need to compare all deltas and they must be in the exact same order we added them
string joinedDeltas = string . Join ( string . Empty , Deltas ) ;
return joinedDeltas . GetHashCode ( ) ;
}
public bool Equals ( Island other )
{
return other . GetHashCode ( ) = = GetHashCode ( ) ;
}
public override bool Equals ( object? obj )
{
return obj ? . GetHashCode ( ) = = GetHashCode ( ) ;
}
}
private const int history_time_max = 5 * 1000 ; // 5 seconds of calculatingRhythmBonus max.
2024-08-24 07:37:58 +08:00
private const double rhythm_multiplier = 0.95 ;
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>
2022-09-07 00:10:32 +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
var island = new Island ( ) ;
var previousIsland = new Island ( ) ;
2024-07-19 13:13:50 +08:00
Dictionary < Island , int > islandCounts = new Dictionary < Island , int > ( ) ;
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 ;
2022-06-13 19:27:02 +08:00
int historicalNoteCount = Math . Min ( current . Index , 32 ) ;
2022-05-28 20:29:09 +08:00
int rhythmStart = 0 ;
while ( rhythmStart < historicalNoteCount - 2 & & current . StartTime - current . Previous ( rhythmStart ) . StartTime < history_time_max )
rhythmStart + + ;
2024-05-19 23:26:51 +08:00
OsuDifficultyHitObject prevObj = ( OsuDifficultyHitObject ) current . Previous ( rhythmStart ) ;
OsuDifficultyHitObject lastObj = ( OsuDifficultyHitObject ) current . Previous ( rhythmStart + 1 ) ;
2022-05-28 20:29:09 +08:00
for ( int i = rhythmStart ; i > 0 ; i - - )
{
OsuDifficultyHitObject currObj = ( OsuDifficultyHitObject ) current . Previous ( i - 1 ) ;
double currHistoricalDecay = ( history_time_max - ( current . StartTime - currObj . StartTime ) ) / history_time_max ; // scales note 0 to 1 from history to now
currHistoricalDecay = Math . Min ( ( double ) ( historicalNoteCount - i ) / historicalNoteCount , currHistoricalDecay ) ; // either we're limited by time or limited by object count.
double currDelta = currObj . StrainTime ;
double prevDelta = prevObj . StrainTime ;
double lastDelta = lastObj . StrainTime ;
2024-07-15 17:45:31 +08:00
2024-08-24 07:37:58 +08:00
double currRatio = 1.0 + 9.8 * Math . Min ( 0.5 , Math . Pow ( Math . Sin ( Math . PI / ( Math . Min ( prevDelta , currDelta ) / Math . Max ( prevDelta , currDelta ) ) ) , 2 ) ) ; // fancy function to calculate rhythmbonuses.
2022-05-28 20:29:09 +08:00
2022-09-07 20:24:54 +08:00
double windowPenalty = Math . Min ( 1 , Math . Max ( 0 , Math . Abs ( prevDelta - currDelta ) - currObj . HitWindowGreat * 0.3 ) / ( currObj . HitWindowGreat * 0.3 ) ) ;
2022-05-28 20:29:09 +08:00
windowPenalty = Math . Min ( 1 , windowPenalty ) ;
double effectiveRatio = windowPenalty * currRatio ;
2024-07-15 17:45:31 +08:00
double deltaDifferenceEpsilon = currObj . HitWindowGreat * 0.3 ;
2022-05-28 20:29:09 +08:00
if ( firstDeltaSwitch )
{
2024-07-15 17:45:31 +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
island . AddDelta ( ( int ) currDelta , deltaDifferenceEpsilon ) ;
2022-05-28 20:29:09 +08:00
}
else
{
2024-08-24 07:37:58 +08:00
// bpm change is into slider, this is easy acc window
if ( currObj . BaseObject is Slider )
2022-05-28 20:29:09 +08:00
effectiveRatio * = 0.125 ;
2024-08-24 07:37:58 +08:00
// 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
// therefore we're checking for quick sliders and don't lower the difficulty for them since they don't really make tapping easier (no time to adjust)
if ( prevObj . BaseObject is Slider & & prevObj . TravelTime > prevDelta * 1.5 )
effectiveRatio * = 0.15 ;
2022-05-28 20:29:09 +08:00
2024-08-24 07:37:58 +08:00
// repeated island polartiy (2 -> 4, 3 -> 5)
if ( island . IsSimilarPolarity ( previousIsland , deltaDifferenceEpsilon ) )
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-07-19 13:13:50 +08:00
if ( ! islandCounts . TryAdd ( island , 1 ) )
2024-07-15 17:45:31 +08:00
{
islandCounts [ island ] + + ;
// repeated island (ex: triplet -> triplet)
2024-07-19 13:13:50 +08:00
double power = logistic ( island . AverageDelta ( ) , 4 , 0.165 , 10 ) ;
2024-08-22 18:59:13 +08:00
effectiveRatio * = Math . Min ( 2.0 / islandCounts [ island ] , Math . Pow ( 1.0 / islandCounts [ island ] , power ) ) ;
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
2022-05-28 20:29:09 +08:00
firstDeltaSwitch = false ; // if we're speeding up, this stays true and we keep counting island size.
2024-07-15 17:45:31 +08:00
island = new Island ( ( int ) currDelta , deltaDifferenceEpsilon ) ;
2022-05-28 20:29:09 +08:00
}
}
2024-07-15 17:45:31 +08:00
else if ( prevDelta > currDelta + deltaDifferenceEpsilon ) // we want to be speeding up.
2022-05-28 20:29:09 +08:00
{
// Begin counting island until we change speed again.
firstDeltaSwitch = true ;
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-08-24 07:37:58 +08:00
return Math . Sqrt ( 4 + rhythmComplexitySum * rhythm_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 ) ) ) ) ;
2022-05-28 20:28:04 +08:00
}
}