2020-07-16 19:38:33 +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.
using System ;
2020-07-21 22:50:54 +08:00
using System.Collections.Concurrent ;
2020-07-16 19:38:33 +08:00
using System.Collections.Generic ;
using System.Diagnostics ;
using System.Linq ;
using System.Threading ;
using System.Threading.Tasks ;
using JetBrains.Annotations ;
using osu.Framework.Allocation ;
2020-07-21 22:13:04 +08:00
using osu.Framework.Bindables ;
2020-07-16 19:38:33 +08:00
using osu.Framework.Graphics.Containers ;
2020-07-21 22:13:04 +08:00
using osu.Framework.Lists ;
2020-07-16 19:38:33 +08:00
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 ) ) ;
2020-07-21 22:13:04 +08:00
// A cache that keeps references to BeatmapInfos for 60sec.
2020-07-21 22:50:54 +08:00
private readonly ConcurrentDictionary < DifficultyCacheLookup , StarDifficulty > difficultyCache = new ConcurrentDictionary < DifficultyCacheLookup , StarDifficulty > ( ) ;
2020-07-16 19:38:33 +08:00
2020-07-21 22:13:04 +08:00
// All bindables that should be updated along with the current ruleset + mods.
private readonly LockedWeakList < BindableStarDifficulty > trackedBindables = new LockedWeakList < BindableStarDifficulty > ( ) ;
[Resolved]
private BeatmapManager beatmapManager { get ; set ; }
[Resolved]
private Bindable < RulesetInfo > currentRuleset { get ; set ; }
[Resolved]
private Bindable < IReadOnlyList < Mod > > currentMods { get ; set ; }
protected override void LoadComplete ( )
{
base . LoadComplete ( ) ;
currentRuleset . BindValueChanged ( _ = > updateTrackedBindables ( ) ) ;
currentMods . BindValueChanged ( _ = > updateTrackedBindables ( ) , true ) ;
}
/// <summary>
/// Retrieves an <see cref="IBindable{StarDifficulty}"/> containing the star difficulty of a <see cref="BeatmapInfo"/> with a given <see cref="RulesetInfo"/> and <see cref="Mod"/> combination.
/// </summary>
/// <remarks>
/// This <see cref="Bindable{StarDifficulty}"/> will not update to follow the currently-selected ruleset and mods.
/// </remarks>
/// <param name="beatmapInfo">The <see cref="BeatmapInfo"/> to get the difficulty of.</param>
/// <param name="rulesetInfo">The <see cref="RulesetInfo"/> to get the difficulty with.</param>
/// <param name="mods">The <see cref="Mod"/>s to get the difficulty with.</param>
/// <param name="cancellationToken">An optional <see cref="CancellationToken"/> which stops updating the star difficulty for the given <see cref="BeatmapInfo"/>.</param>
/// <returns>An <see cref="IBindable{StarDifficulty}"/> that is updated to contain the star difficulty when it becomes available.</returns>
public IBindable < StarDifficulty > GetUntrackedBindable ( [ NotNull ] BeatmapInfo beatmapInfo , [ CanBeNull ] RulesetInfo rulesetInfo = null , [ CanBeNull ] IReadOnlyList < Mod > mods = null ,
CancellationToken cancellationToken = default )
= > createBindable ( beatmapInfo , rulesetInfo , mods , cancellationToken ) ;
/// <summary>
/// Retrieves a <see cref="IBindable{StarDifficulty}"/> containing the star difficulty of a <see cref="BeatmapInfo"/> that follows the user's currently-selected ruleset and mods.
/// </summary>
/// <remarks>
/// Ensure to hold a local reference of the returned <see cref="Bindable{StarDifficulty}"/> in order to receive value-changed events.
/// </remarks>
/// <param name="beatmapInfo">The <see cref="BeatmapInfo"/> to get the difficulty of.</param>
/// <param name="cancellationToken">An optional <see cref="CancellationToken"/> which stops updating the star difficulty for the given <see cref="BeatmapInfo"/>.</param>
/// <returns>An <see cref="IBindable{StarDifficulty}"/> that is updated to contain the star difficulty when it becomes available, or when the currently-selected ruleset and mods change.</returns>
public IBindable < StarDifficulty > GetTrackedBindable ( [ NotNull ] BeatmapInfo beatmapInfo , CancellationToken cancellationToken = default )
2020-07-16 19:38:33 +08:00
{
2020-07-21 22:13:04 +08:00
var bindable = createBindable ( beatmapInfo , currentRuleset . Value , currentMods . Value , cancellationToken ) ;
trackedBindables . Add ( bindable ) ;
return bindable ;
2020-07-16 19:38:33 +08:00
}
2020-07-21 22:13:04 +08:00
/// <summary>
/// Retrieves the difficulty of a <see cref="BeatmapInfo"/>.
/// </summary>
/// <param name="beatmapInfo">The <see cref="BeatmapInfo"/> to get the difficulty of.</param>
/// <param name="rulesetInfo">The <see cref="RulesetInfo"/> to get the difficulty with.</param>
/// <param name="mods">The <see cref="Mod"/>s to get the difficulty with.</param>
/// <param name="cancellationToken">An optional <see cref="CancellationToken"/> which stops computing the star difficulty.</param>
/// <returns>The <see cref="StarDifficulty"/>.</returns>
public async Task < StarDifficulty > GetDifficultyAsync ( [ NotNull ] BeatmapInfo beatmapInfo , [ CanBeNull ] RulesetInfo rulesetInfo = null , [ CanBeNull ] IReadOnlyList < Mod > mods = null ,
CancellationToken cancellationToken = default )
2020-07-16 19:38:33 +08:00
{
2020-07-16 20:07:14 +08:00
if ( tryGetGetExisting ( beatmapInfo , rulesetInfo , mods , out var existing , out var key ) )
return existing ;
2020-07-16 19:38:33 +08:00
2020-07-21 22:50:54 +08:00
return await Task . Factory . StartNew ( ( ) = > computeDifficulty ( key , beatmapInfo , rulesetInfo ) , cancellationToken ,
TaskCreationOptions . HideScheduler | TaskCreationOptions . RunContinuationsAsynchronously , updateScheduler ) ;
2020-07-16 20:07:14 +08:00
}
2020-07-16 19:38:33 +08:00
2020-07-21 22:13:04 +08:00
/// <summary>
/// Retrieves the difficulty of a <see cref="BeatmapInfo"/>.
/// </summary>
/// <param name="beatmapInfo">The <see cref="BeatmapInfo"/> to get the difficulty of.</param>
/// <param name="rulesetInfo">The <see cref="RulesetInfo"/> to get the difficulty with.</param>
/// <param name="mods">The <see cref="Mod"/>s to get the difficulty with.</param>
/// <returns>The <see cref="StarDifficulty"/>.</returns>
public StarDifficulty GetDifficulty ( [ NotNull ] BeatmapInfo beatmapInfo , [ CanBeNull ] RulesetInfo rulesetInfo = null , [ CanBeNull ] IReadOnlyList < Mod > mods = null )
2020-07-16 20:07:14 +08:00
{
if ( tryGetGetExisting ( beatmapInfo , rulesetInfo , mods , out var existing , out var key ) )
2020-07-16 19:38:33 +08:00
return existing ;
2020-07-21 22:50:54 +08:00
return computeDifficulty ( key , beatmapInfo , rulesetInfo ) ;
2020-07-16 20:07:14 +08:00
}
2020-07-21 22:13:04 +08:00
private CancellationTokenSource trackedUpdateCancellationSource ;
/// <summary>
/// Updates all tracked <see cref="BindableStarDifficulty"/> using the current ruleset and mods.
/// </summary>
private void updateTrackedBindables ( )
{
trackedUpdateCancellationSource ? . Cancel ( ) ;
trackedUpdateCancellationSource = new CancellationTokenSource ( ) ;
foreach ( var b in trackedBindables )
{
if ( trackedUpdateCancellationSource . IsCancellationRequested )
break ;
using ( var linkedSource = CancellationTokenSource . CreateLinkedTokenSource ( trackedUpdateCancellationSource . Token , b . CancellationToken ) )
updateBindable ( b , currentRuleset . Value , currentMods . Value , linkedSource . Token ) ;
}
}
/// <summary>
/// Updates the value of a <see cref="BindableStarDifficulty"/> with a given ruleset + mods.
/// </summary>
/// <param name="bindable">The <see cref="BindableStarDifficulty"/> to update.</param>
/// <param name="rulesetInfo">The <see cref="RulesetInfo"/> to update with.</param>
/// <param name="mods">The <see cref="Mod"/>s to update with.</param>
/// <param name="cancellationToken">A token that may be used to cancel this update.</param>
private void updateBindable ( [ NotNull ] BindableStarDifficulty bindable , [ CanBeNull ] RulesetInfo rulesetInfo , [ CanBeNull ] IReadOnlyList < Mod > mods , CancellationToken cancellationToken = default )
{
GetDifficultyAsync ( bindable . Beatmap , rulesetInfo , mods , cancellationToken ) . ContinueWith ( t = >
{
// We're on a threadpool thread, but we should exit back to the update thread so consumers can safely handle value-changed events.
Schedule ( ( ) = >
{
if ( ! cancellationToken . IsCancellationRequested )
bindable . Value = t . Result ;
} ) ;
} , cancellationToken ) ;
}
/// <summary>
/// Creates a new <see cref="BindableStarDifficulty"/> and triggers an initial value update.
/// </summary>
/// <param name="beatmapInfo">The <see cref="BeatmapInfo"/> that star difficulty should correspond to.</param>
/// <param name="initialRulesetInfo">The initial <see cref="RulesetInfo"/> to get the difficulty with.</param>
/// <param name="initialMods">The initial <see cref="Mod"/>s to get the difficulty with.</param>
/// <param name="cancellationToken">An optional <see cref="CancellationToken"/> which stops updating the star difficulty for the given <see cref="BeatmapInfo"/>.</param>
/// <returns>The <see cref="BindableStarDifficulty"/>.</returns>
private BindableStarDifficulty createBindable ( [ NotNull ] BeatmapInfo beatmapInfo , [ CanBeNull ] RulesetInfo initialRulesetInfo , [ CanBeNull ] IReadOnlyList < Mod > initialMods ,
CancellationToken cancellationToken )
{
var bindable = new BindableStarDifficulty ( beatmapInfo , cancellationToken ) ;
updateBindable ( bindable , initialRulesetInfo , initialMods , cancellationToken ) ;
return bindable ;
}
/// <summary>
/// Computes the difficulty defined by a <see cref="DifficultyCacheLookup"/> key, and stores it to the timed cache.
/// </summary>
/// <param name="key">The <see cref="DifficultyCacheLookup"/> that defines the computation parameters.</param>
2020-07-21 22:50:54 +08:00
/// <param name="beatmapInfo">The <see cref="BeatmapInfo"/> to compute the difficulty of.</param>
/// <param name="rulesetInfo">The <see cref="RulesetInfo"/> to compute the difficulty with.</param>
2020-07-21 22:13:04 +08:00
/// <returns>The <see cref="StarDifficulty"/>.</returns>
2020-07-21 22:50:54 +08:00
private StarDifficulty computeDifficulty ( in DifficultyCacheLookup key , BeatmapInfo beatmapInfo , RulesetInfo rulesetInfo )
2020-07-16 20:07:14 +08:00
{
2020-07-16 19:38:33 +08:00
try
{
2020-07-21 22:50:54 +08:00
var ruleset = rulesetInfo . CreateInstance ( ) ;
2020-07-16 19:38:33 +08:00
Debug . Assert ( ruleset ! = null ) ;
2020-07-21 22:50:54 +08:00
var calculator = ruleset . CreateDifficultyCalculator ( beatmapManager . GetWorkingBeatmap ( beatmapInfo ) ) ;
2020-07-16 20:07:14 +08:00
var attributes = calculator . Calculate ( key . Mods ) ;
2020-07-16 19:38:33 +08:00
2020-07-21 22:50:54 +08:00
return difficultyCache [ key ] = new StarDifficulty ( attributes . StarRating ) ;
2020-07-16 19:38:33 +08:00
}
catch
{
2020-07-21 22:50:54 +08:00
return difficultyCache [ key ] = new StarDifficulty ( 0 ) ;
2020-07-16 19:38:33 +08:00
}
}
2020-07-16 20:07:14 +08: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>
2020-07-21 22:13:04 +08:00
/// <param name="key">The <see cref="DifficultyCacheLookup"/> key that was used to perform this lookup. This can be further used to query <see cref="computeDifficulty"/>.</param>
2020-07-16 20:07:14 +08:00
/// <returns>Whether an existing difficulty was found.</returns>
2020-07-21 22:13:04 +08:00
private bool tryGetGetExisting ( BeatmapInfo beatmapInfo , RulesetInfo rulesetInfo , IReadOnlyList < Mod > mods , out StarDifficulty existingDifficulty , out DifficultyCacheLookup key )
2020-07-16 20:07:14 +08:00
{
// In the case that the user hasn't given us a ruleset, use the beatmap's default ruleset.
rulesetInfo ? ? = beatmapInfo . Ruleset ;
2020-07-21 22:50:54 +08:00
// Difficulty can only be computed if the beatmap and ruleset are locally available.
if ( beatmapInfo . ID = = 0 | | rulesetInfo . ID = = null )
2020-07-16 20:07:14 +08:00
{
2020-07-21 22:13:04 +08:00
existingDifficulty = new StarDifficulty ( 0 ) ;
2020-07-16 20:07:14 +08:00
key = default ;
return true ;
}
2020-07-21 22:50:54 +08:00
key = new DifficultyCacheLookup ( beatmapInfo . ID , rulesetInfo . ID . Value , mods ) ;
2020-07-16 20:07:14 +08:00
return difficultyCache . TryGetValue ( key , out existingDifficulty ) ;
}
2020-07-16 19:38:33 +08:00
private readonly struct DifficultyCacheLookup : IEquatable < DifficultyCacheLookup >
{
2020-07-21 22:50:54 +08:00
public readonly int BeatmapId ;
public readonly int RulesetId ;
2020-07-16 20:07:14 +08:00
public readonly Mod [ ] Mods ;
2020-07-16 19:38:33 +08:00
2020-07-21 22:50:54 +08:00
public DifficultyCacheLookup ( int beatmapId , int rulesetId , IEnumerable < Mod > mods )
2020-07-16 19:38:33 +08:00
{
2020-07-21 22:50:54 +08:00
BeatmapId = beatmapId ;
RulesetId = rulesetId ;
2020-07-16 20:07:14 +08:00
Mods = mods ? . OrderBy ( m = > m . Acronym ) . ToArray ( ) ? ? Array . Empty < Mod > ( ) ;
2020-07-16 19:38:33 +08:00
}
public bool Equals ( DifficultyCacheLookup other )
2020-07-21 22:50:54 +08:00
= > BeatmapId = = other . BeatmapId
& & RulesetId = = other . RulesetId
2020-07-16 20:07:14 +08:00
& & Mods . SequenceEqual ( other . Mods ) ;
2020-07-16 19:38:33 +08:00
public override int GetHashCode ( )
{
var hashCode = new HashCode ( ) ;
2020-07-21 22:50:54 +08:00
hashCode . Add ( BeatmapId ) ;
hashCode . Add ( RulesetId ) ;
2020-07-16 20:07:14 +08:00
foreach ( var mod in Mods )
2020-07-16 19:38:33 +08:00
hashCode . Add ( mod . Acronym ) ;
return hashCode . ToHashCode ( ) ;
}
}
2020-07-21 22:13:04 +08:00
private class BindableStarDifficulty : Bindable < StarDifficulty >
{
public readonly BeatmapInfo Beatmap ;
public readonly CancellationToken CancellationToken ;
public BindableStarDifficulty ( BeatmapInfo beatmap , CancellationToken cancellationToken )
{
Beatmap = beatmap ;
CancellationToken = cancellationToken ;
}
}
}
public readonly struct StarDifficulty
{
public readonly double Stars ;
public StarDifficulty ( double stars )
{
Stars = stars ;
// Todo: Add more members (BeatmapInfo.DifficultyRating? Attributes? Etc...)
}
2020-07-16 19:38:33 +08:00
}
}