2020-07-16 11:38:33 +00: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.
using System ;
using System.Collections.Generic ;
using System.Diagnostics ;
using System.Linq ;
using System.Threading ;
using System.Threading.Tasks ;
using JetBrains.Annotations ;
using osu.Framework.Allocation ;
using osu.Framework.Graphics.Containers ;
using osu.Framework.Threading ;
using osu.Game.Rulesets ;
using osu.Game.Rulesets.Mods ;
namespace osu.Game.Beatmaps
{
public class BeatmapDifficultyManager : CompositeDrawable
{
// Too many simultaneous updates can lead to stutters. One thread seems to work fine for song select display purposes.
private readonly ThreadedTaskScheduler updateScheduler = new ThreadedTaskScheduler ( 1 , nameof ( BeatmapDifficultyManager ) ) ;
private readonly TimedExpiryCache < DifficultyCacheLookup , double > difficultyCache = new TimedExpiryCache < DifficultyCacheLookup , double > { ExpiryTime = 60000 } ;
private readonly BeatmapManager beatmapManager ;
public BeatmapDifficultyManager ( BeatmapManager beatmapManager )
{
this . beatmapManager = beatmapManager ;
}
2020-07-16 12:07:14 +00:00
public async Task < double > GetDifficultyAsync ( [ NotNull ] BeatmapInfo beatmapInfo , [ CanBeNull ] RulesetInfo rulesetInfo = null , [ CanBeNull ] IReadOnlyList < Mod > mods = null ,
CancellationToken cancellationToken = default )
2020-07-16 11:38:33 +00:00
{
2020-07-16 12:07:14 +00:00
if ( tryGetGetExisting ( beatmapInfo , rulesetInfo , mods , out var existing , out var key ) )
return existing ;
2020-07-16 11:38:33 +00:00
2020-07-16 12:07:14 +00:00
return await Task . Factory . StartNew ( ( ) = > getDifficulty ( key ) , cancellationToken , TaskCreationOptions . HideScheduler | TaskCreationOptions . RunContinuationsAsynchronously , updateScheduler ) ;
}
2020-07-16 11:38:33 +00:00
2020-07-16 12:07:14 +00:00
public double GetDifficulty ( [ NotNull ] BeatmapInfo beatmapInfo , [ CanBeNull ] RulesetInfo rulesetInfo = null , [ CanBeNull ] IReadOnlyList < Mod > mods = null )
{
if ( tryGetGetExisting ( beatmapInfo , rulesetInfo , mods , out var existing , out var key ) )
2020-07-16 11:38:33 +00:00
return existing ;
2020-07-16 12:07:14 +00:00
return getDifficulty ( key ) ;
}
private double getDifficulty ( in DifficultyCacheLookup key )
{
2020-07-16 11:38:33 +00:00
try
{
2020-07-16 12:07:14 +00:00
var ruleset = key . RulesetInfo . CreateInstance ( ) ;
2020-07-16 11:38:33 +00:00
Debug . Assert ( ruleset ! = null ) ;
2020-07-16 12:07:14 +00:00
var calculator = ruleset . CreateDifficultyCalculator ( beatmapManager . GetWorkingBeatmap ( key . BeatmapInfo ) ) ;
var attributes = calculator . Calculate ( key . Mods ) ;
2020-07-16 11:38:33 +00:00
difficultyCache . Add ( key , attributes . StarRating ) ;
return attributes . StarRating ;
}
catch
{
difficultyCache . Add ( key , 0 ) ;
return 0 ;
}
}
2020-07-16 12:07:14 +00:00
/// <summary>
/// Attempts to retrieve an existing difficulty for the combination.
/// </summary>
/// <param name="beatmapInfo">The <see cref="BeatmapInfo"/>.</param>
/// <param name="rulesetInfo">The <see cref="RulesetInfo"/>.</param>
/// <param name="mods">The <see cref="Mod"/>s.</param>
/// <param name="existingDifficulty">The existing difficulty value, if present.</param>
/// <param name="key">The <see cref="DifficultyCacheLookup"/> key that was used to perform this lookup. This can be further used to query <see cref="getDifficulty"/>.</param>
/// <returns>Whether an existing difficulty was found.</returns>
private bool tryGetGetExisting ( BeatmapInfo beatmapInfo , RulesetInfo rulesetInfo , IReadOnlyList < Mod > mods , out double existingDifficulty , out DifficultyCacheLookup key )
{
// In the case that the user hasn't given us a ruleset, use the beatmap's default ruleset.
rulesetInfo ? ? = beatmapInfo . Ruleset ;
// Difficulty can only be computed if the beatmap is locally available.
if ( beatmapInfo . ID = = 0 )
{
existingDifficulty = 0 ;
key = default ;
return true ;
}
key = new DifficultyCacheLookup ( beatmapInfo , rulesetInfo , mods ) ;
return difficultyCache . TryGetValue ( key , out existingDifficulty ) ;
}
2020-07-16 11:38:33 +00:00
private readonly struct DifficultyCacheLookup : IEquatable < DifficultyCacheLookup >
{
2020-07-16 12:07:14 +00:00
public readonly BeatmapInfo BeatmapInfo ;
public readonly RulesetInfo RulesetInfo ;
public readonly Mod [ ] Mods ;
2020-07-16 11:38:33 +00:00
public DifficultyCacheLookup ( BeatmapInfo beatmapInfo , RulesetInfo rulesetInfo , IEnumerable < Mod > mods )
{
2020-07-16 12:07:14 +00:00
BeatmapInfo = beatmapInfo ;
RulesetInfo = rulesetInfo ;
Mods = mods ? . OrderBy ( m = > m . Acronym ) . ToArray ( ) ? ? Array . Empty < Mod > ( ) ;
2020-07-16 11:38:33 +00:00
}
public bool Equals ( DifficultyCacheLookup other )
2020-07-16 12:07:14 +00:00
= > BeatmapInfo . Equals ( other . BeatmapInfo )
& & Mods . SequenceEqual ( other . Mods ) ;
2020-07-16 11:38:33 +00:00
public override int GetHashCode ( )
{
var hashCode = new HashCode ( ) ;
2020-07-16 12:07:14 +00:00
hashCode . Add ( BeatmapInfo . Hash ) ;
hashCode . Add ( RulesetInfo . GetHashCode ( ) ) ;
foreach ( var mod in Mods )
2020-07-16 11:38:33 +00:00
hashCode . Add ( mod . Acronym ) ;
return hashCode . ToHashCode ( ) ;
}
}
}
}