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 ;
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 ) ;
}
public double AverageDelta ( ) = > Math . Max ( Deltas . Average ( ) , OsuDifficultyHitObject . MIN_DELTA_TIME ) ;
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.
private const double rhythm_multiplier = 1.14 ;
private const int max_island_size = 7 ;
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
{
2024-07-15 17:45:31 +08:00
Dictionary < Island , int > islandCounts = new Dictionary < Island , int > ( ) ;
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 ( ) ;
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
2022-05-28 20:29:09 +08:00
double currRatio = 1.0 + 6.0 * 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-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-07-15 17:45:31 +08:00
if ( island . Deltas . Count < max_island_size )
{
// island is still progressing
island . AddDelta ( ( int ) currDelta , deltaDifferenceEpsilon ) ;
}
2022-05-28 20:29:09 +08:00
}
else
{
2024-05-19 20:28:46 +08:00
if ( currObj . BaseObject is Slider ) // bpm change is into slider, this is easy acc window
2022-05-28 20:29:09 +08:00
effectiveRatio * = 0.125 ;
2024-05-19 20:28:46 +08:00
if ( prevObj . BaseObject is Slider ) // bpm change was from a slider, this is easier typically than circle -> circle
2022-05-28 20:29:09 +08:00
effectiveRatio * = 0.25 ;
2024-07-15 17:45:31 +08:00
if ( previousIsland . Deltas . Count % 2 = = island . Deltas . Count % 2 ) // repeated island polartiy (2 -> 4, 3 -> 5)
2022-05-28 20:29:09 +08:00
effectiveRatio * = 0.50 ;
2024-07-15 17:45:31 +08:00
if ( lastDelta > prevDelta + deltaDifferenceEpsilon & & prevDelta > currDelta + deltaDifferenceEpsilon ) // previous increase happened a note ago, 1/1->1/2-1/4, dont want to buff this.
2022-05-28 20:29:09 +08:00
effectiveRatio * = 0.125 ;
2024-07-15 17:45:31 +08:00
if ( islandCounts . ContainsKey ( island ) )
{
islandCounts [ island ] + + ;
// repeated island (ex: triplet -> triplet)
double power = Math . Max ( 0.75 , logistic ( island . AverageDelta ( ) , 3 , 0.15 , 9 ) ) ;
effectiveRatio * = Math . Pow ( 1.0 / islandCounts [ island ] , power ) ;
}
else
{
islandCounts . Add ( island , 1 ) ;
}
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
}
return Math . Sqrt ( 4 + rhythmComplexitySum * rhythm_multiplier ) / 2 ; //produces multiplier that can be applied to strain. range [1, infinity) (not really though)
}
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
}
}