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 ;
2020-07-21 14:13:04 +00:00
using osu.Framework.Bindables ;
using osu.Framework.Lists ;
2020-10-12 07:31:42 +00:00
using osu.Framework.Logging ;
2020-07-16 11:38:33 +00:00
using osu.Framework.Threading ;
2020-10-10 16:15:52 +00:00
using osu.Framework.Utils ;
2020-11-06 04:26:18 +00:00
using osu.Game.Database ;
2020-07-16 11:38:33 +00:00
using osu.Game.Rulesets ;
using osu.Game.Rulesets.Mods ;
2020-10-12 07:31:42 +00:00
using osu.Game.Rulesets.UI ;
2020-07-16 11:38:33 +00:00
namespace osu.Game.Beatmaps
{
2020-11-06 04:14:23 +00:00
/// <summary>
/// A component which performs and acts as a central cache for difficulty calculations of beatmap/ruleset/mod combinations.
/// Currently not persisted between game sessions.
/// </summary>
2020-11-06 04:26:18 +00:00
public class BeatmapDifficultyCache : MemoryCachingComponent < BeatmapDifficultyCache . DifficultyCacheLookup , StarDifficulty >
2020-07-16 11:38:33 +00:00
{
// Too many simultaneous updates can lead to stutters. One thread seems to work fine for song select display purposes.
2020-11-06 04:14:23 +00:00
private readonly ThreadedTaskScheduler updateScheduler = new ThreadedTaskScheduler ( 1 , nameof ( BeatmapDifficultyCache ) ) ;
2020-07-16 11:38:33 +00:00
2020-07-21 14:13:04 +00:00
// All bindables that should be updated along with the current ruleset + mods.
2020-11-09 16:10:00 +00:00
private readonly WeakList < BindableStarDifficulty > trackedBindables = new WeakList < BindableStarDifficulty > ( ) ;
2020-07-21 14:13:04 +00:00
[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>
2020-07-24 04:52:43 +00:00
/// Retrieves a bindable containing the star difficulty of a <see cref="BeatmapInfo"/> that follows the currently-selected ruleset and mods.
2020-07-21 14:13:04 +00:00
/// </summary>
/// <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>
2020-07-24 04:38:53 +00:00
/// <returns>A bindable that is updated to contain the star difficulty when it becomes available.</returns>
2020-07-24 04:52:43 +00:00
public IBindable < StarDifficulty > GetBindableDifficulty ( [ NotNull ] BeatmapInfo beatmapInfo , CancellationToken cancellationToken = default )
{
var bindable = createBindable ( beatmapInfo , currentRuleset . Value , currentMods . Value , cancellationToken ) ;
2020-11-09 16:10:00 +00:00
lock ( trackedBindables )
trackedBindables . Add ( bindable ) ;
2020-07-24 04:52:43 +00:00
return bindable ;
}
2020-07-21 14:13:04 +00:00
/// <summary>
2020-07-24 04:52:43 +00:00
/// Retrieves a bindable containing the star difficulty of a <see cref="BeatmapInfo"/> with a given <see cref="RulesetInfo"/> and <see cref="Mod"/> combination.
2020-07-21 14:13:04 +00:00
/// </summary>
/// <remarks>
2020-07-24 04:52:43 +00:00
/// The bindable will not update to follow the currently-selected ruleset and mods.
2020-07-21 14:13:04 +00:00
/// </remarks>
/// <param name="beatmapInfo">The <see cref="BeatmapInfo"/> to get the difficulty of.</param>
2020-07-24 04:54:47 +00:00
/// <param name="rulesetInfo">The <see cref="RulesetInfo"/> to get the difficulty with. If <c>null</c>, the <paramref name="beatmapInfo"/>'s ruleset is used.</param>
/// <param name="mods">The <see cref="Mod"/>s to get the difficulty with. If <c>null</c>, no mods will be assumed.</param>
2020-07-21 14:13:04 +00:00
/// <param name="cancellationToken">An optional <see cref="CancellationToken"/> which stops updating the star difficulty for the given <see cref="BeatmapInfo"/>.</param>
2020-07-24 04:52:43 +00:00
/// <returns>A bindable that is updated to contain the star difficulty when it becomes available.</returns>
2020-07-24 04:54:47 +00:00
public IBindable < StarDifficulty > GetBindableDifficulty ( [ NotNull ] BeatmapInfo beatmapInfo , [ CanBeNull ] RulesetInfo rulesetInfo , [ CanBeNull ] IEnumerable < Mod > mods ,
2020-07-24 04:52:43 +00:00
CancellationToken cancellationToken = default )
= > createBindable ( beatmapInfo , rulesetInfo , mods , cancellationToken ) ;
2020-07-16 11:38:33 +00:00
2020-07-21 14:13:04 +00: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>
2020-11-09 16:10:00 +00:00
public Task < StarDifficulty > GetDifficultyAsync ( [ NotNull ] BeatmapInfo beatmapInfo , [ CanBeNull ] RulesetInfo rulesetInfo = null , [ CanBeNull ] IEnumerable < Mod > mods = null ,
CancellationToken cancellationToken = default )
2020-07-16 11:38:33 +00:00
{
2020-11-06 05:31:21 +00:00
// In the case that the user hasn't given us a ruleset, use the beatmap's default ruleset.
rulesetInfo ? ? = beatmapInfo . Ruleset ;
2020-07-16 11:38:33 +00:00
2020-11-06 05:31:21 +00:00
// Difficulty can only be computed if the beatmap and ruleset are locally available.
if ( beatmapInfo . ID = = 0 | | rulesetInfo . ID = = null )
2020-08-28 13:08:28 +00:00
{
2020-11-06 05:31:21 +00:00
// If not, fall back to the existing star difficulty (e.g. from an online source).
return Task . FromResult ( new StarDifficulty ( beatmapInfo . StarDifficulty , beatmapInfo . MaxCombo ? ? 0 ) ) ;
}
return GetAsync ( new DifficultyCacheLookup ( beatmapInfo , rulesetInfo , mods ) , cancellationToken ) ;
}
protected override Task < StarDifficulty > ComputeValueAsync ( DifficultyCacheLookup lookup , CancellationToken token = default )
{
return Task . Factory . StartNew ( ( ) = >
{
if ( CheckExists ( lookup , out var existing ) )
2020-08-28 13:08:28 +00:00
return existing ;
2020-11-06 05:31:21 +00:00
return computeDifficulty ( lookup ) ;
} , token , TaskCreationOptions . HideScheduler | TaskCreationOptions . RunContinuationsAsynchronously , updateScheduler ) ;
2020-07-16 12:07:14 +00:00
}
2020-07-16 11:38:33 +00:00
2020-10-01 11:50:47 +00:00
/// <summary>
/// Retrieves the <see cref="DifficultyRating"/> that describes a star rating.
/// </summary>
/// <remarks>
/// For more information, see: https://osu.ppy.sh/help/wiki/Difficulties
/// </remarks>
/// <param name="starRating">The star rating.</param>
/// <returns>The <see cref="DifficultyRating"/> that best describes <paramref name="starRating"/>.</returns>
public static DifficultyRating GetDifficultyRating ( double starRating )
{
2020-10-10 16:15:52 +00:00
if ( Precision . AlmostBigger ( starRating , 6.5 , 0.005 ) )
return DifficultyRating . ExpertPlus ;
2020-10-01 11:50:47 +00:00
2020-10-10 16:15:52 +00:00
if ( Precision . AlmostBigger ( starRating , 5.3 , 0.005 ) )
return DifficultyRating . Expert ;
if ( Precision . AlmostBigger ( starRating , 4.0 , 0.005 ) )
return DifficultyRating . Insane ;
if ( Precision . AlmostBigger ( starRating , 2.7 , 0.005 ) )
return DifficultyRating . Hard ;
if ( Precision . AlmostBigger ( starRating , 2.0 , 0.005 ) )
return DifficultyRating . Normal ;
return DifficultyRating . Easy ;
2020-10-01 11:50:47 +00:00
}
2020-07-21 14:13:04 +00:00
private CancellationTokenSource trackedUpdateCancellationSource ;
2020-07-28 07:52:07 +00:00
private readonly List < CancellationTokenSource > linkedCancellationSources = new List < CancellationTokenSource > ( ) ;
2020-07-21 14:13:04 +00:00
/// <summary>
/// Updates all tracked <see cref="BindableStarDifficulty"/> using the current ruleset and mods.
/// </summary>
private void updateTrackedBindables ( )
{
2020-11-09 16:10:00 +00:00
lock ( trackedBindables )
2020-07-21 14:13:04 +00:00
{
2020-11-09 16:10:00 +00:00
cancelTrackedBindableUpdate ( ) ;
trackedUpdateCancellationSource = new CancellationTokenSource ( ) ;
2020-07-28 07:52:07 +00:00
2020-11-09 16:10:00 +00:00
foreach ( var b in trackedBindables )
{
var linkedSource = CancellationTokenSource . CreateLinkedTokenSource ( trackedUpdateCancellationSource . Token , b . CancellationToken ) ;
linkedCancellationSources . Add ( linkedSource ) ;
updateBindable ( b , currentRuleset . Value , currentMods . Value , linkedSource . Token ) ;
}
2020-07-28 07:52:07 +00:00
}
}
/// <summary>
/// Cancels the existing update of all tracked <see cref="BindableStarDifficulty"/> via <see cref="updateTrackedBindables"/>.
/// </summary>
private void cancelTrackedBindableUpdate ( )
{
2020-11-09 16:10:00 +00:00
lock ( trackedBindables )
2020-07-29 02:30:25 +00:00
{
2020-11-09 16:10:00 +00:00
trackedUpdateCancellationSource ? . Cancel ( ) ;
trackedUpdateCancellationSource = null ;
if ( linkedCancellationSources ! = null )
{
foreach ( var c in linkedCancellationSources )
c . Dispose ( ) ;
2020-07-28 07:52:07 +00:00
2020-11-09 16:10:00 +00:00
linkedCancellationSources . Clear ( ) ;
}
2020-07-29 02:30:25 +00:00
}
2020-07-21 14:13:04 +00:00
}
2020-07-28 07:52:19 +00:00
/// <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 ] IEnumerable < Mod > initialMods ,
CancellationToken cancellationToken )
{
var bindable = new BindableStarDifficulty ( beatmapInfo , cancellationToken ) ;
updateBindable ( bindable , initialRulesetInfo , initialMods , cancellationToken ) ;
return bindable ;
}
2020-07-21 14:13:04 +00:00
/// <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>
2020-07-24 04:40:01 +00:00
private void updateBindable ( [ NotNull ] BindableStarDifficulty bindable , [ CanBeNull ] RulesetInfo rulesetInfo , [ CanBeNull ] IEnumerable < Mod > mods , CancellationToken cancellationToken = default )
2020-07-21 14:13:04 +00:00
{
2020-11-07 23:12:25 +00:00
// GetDifficultyAsync will fall back to existing data from BeatmapInfo if not locally available
// (contrary to GetAsync)
GetDifficultyAsync ( bindable . Beatmap , rulesetInfo , mods , cancellationToken )
2020-11-06 05:31:21 +00:00
. ContinueWith ( t = >
2020-07-21 14:13:04 +00:00
{
2020-11-06 05:31:21 +00:00
// 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 ) ;
2020-07-21 14:13:04 +00:00
}
/// <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>
/// <returns>The <see cref="StarDifficulty"/>.</returns>
2020-11-06 05:31:21 +00:00
private StarDifficulty computeDifficulty ( in DifficultyCacheLookup key )
2020-07-16 12:07:14 +00:00
{
2020-07-22 03:48:12 +00:00
// In the case that the user hasn't given us a ruleset, use the beatmap's default ruleset.
2020-11-06 05:31:21 +00:00
var beatmapInfo = key . Beatmap ;
var rulesetInfo = key . Ruleset ;
2020-07-22 03:48:12 +00:00
2020-07-16 11:38:33 +00:00
try
{
2020-07-21 14:50:54 +00:00
var ruleset = rulesetInfo . CreateInstance ( ) ;
2020-07-16 11:38:33 +00:00
Debug . Assert ( ruleset ! = null ) ;
2020-11-06 05:31:21 +00:00
var calculator = ruleset . CreateDifficultyCalculator ( beatmapManager . GetWorkingBeatmap ( key . Beatmap ) ) ;
var attributes = calculator . Calculate ( key . OrderedMods ) ;
2020-07-16 11:38:33 +00:00
2020-11-06 05:31:21 +00:00
return new StarDifficulty ( attributes ) ;
2020-07-16 11:38:33 +00:00
}
2020-10-12 07:31:42 +00:00
catch ( BeatmapInvalidForRulesetException e )
{
// Conversion has failed for the given ruleset, so return the difficulty in the beatmap's default ruleset.
// Ensure the beatmap's default ruleset isn't the one already being converted to.
// This shouldn't happen as it means something went seriously wrong, but if it does an endless loop should be avoided.
if ( rulesetInfo . Equals ( beatmapInfo . Ruleset ) )
{
Logger . Error ( e , $"Failed to convert {beatmapInfo.OnlineBeatmapID} to the beatmap's default ruleset ({beatmapInfo.Ruleset})." ) ;
2020-11-06 05:31:21 +00:00
return new StarDifficulty ( ) ;
2020-10-12 07:31:42 +00:00
}
2020-11-06 05:31:21 +00:00
return GetAsync ( new DifficultyCacheLookup ( key . Beatmap , key . Beatmap . Ruleset , key . OrderedMods ) ) . Result ;
2020-10-12 07:31:42 +00:00
}
2020-07-16 11:38:33 +00:00
catch
{
2020-11-06 05:31:21 +00:00
return new StarDifficulty ( ) ;
2020-07-16 12:07:14 +00:00
}
}
2020-07-28 07:52:07 +00:00
protected override void Dispose ( bool isDisposing )
{
base . Dispose ( isDisposing ) ;
2020-07-28 08:23:35 +00:00
2020-07-28 07:52:07 +00:00
cancelTrackedBindableUpdate ( ) ;
2020-07-29 02:30:25 +00:00
updateScheduler ? . Dispose ( ) ;
2020-07-28 07:52:07 +00:00
}
2020-08-28 13:12:17 +00:00
public readonly struct DifficultyCacheLookup : IEquatable < DifficultyCacheLookup >
2020-07-16 11:38:33 +00:00
{
2020-11-06 05:31:21 +00:00
public readonly BeatmapInfo Beatmap ;
public readonly RulesetInfo Ruleset ;
public readonly Mod [ ] OrderedMods ;
2020-07-16 11:38:33 +00:00
2020-11-06 08:24:28 +00:00
public DifficultyCacheLookup ( [ NotNull ] BeatmapInfo beatmap , [ CanBeNull ] RulesetInfo ruleset , IEnumerable < Mod > mods )
2020-07-16 11:38:33 +00:00
{
2020-11-06 05:31:21 +00:00
Beatmap = beatmap ;
2020-11-06 07:58:53 +00:00
// In the case that the user hasn't given us a ruleset, use the beatmap's default ruleset.
Ruleset = ruleset ? ? Beatmap . Ruleset ;
2020-11-06 05:31:21 +00:00
OrderedMods = mods ? . OrderBy ( m = > m . Acronym ) . ToArray ( ) ? ? Array . Empty < Mod > ( ) ;
2020-07-16 11:38:33 +00:00
}
public bool Equals ( DifficultyCacheLookup other )
2020-11-06 05:31:21 +00:00
= > Beatmap . ID = = other . Beatmap . ID
& & Ruleset . ID = = other . Ruleset . ID
& & OrderedMods . Select ( m = > m . Acronym ) . SequenceEqual ( other . OrderedMods . Select ( m = > m . Acronym ) ) ;
2020-07-16 11:38:33 +00:00
public override int GetHashCode ( )
{
var hashCode = new HashCode ( ) ;
2020-11-06 05:31:21 +00:00
hashCode . Add ( Beatmap . ID ) ;
hashCode . Add ( Ruleset . ID ) ;
foreach ( var mod in OrderedMods )
2020-07-16 11:38:33 +00:00
hashCode . Add ( mod . Acronym ) ;
return hashCode . ToHashCode ( ) ;
}
}
2020-07-21 14:13:04 +00:00
private class BindableStarDifficulty : Bindable < StarDifficulty >
{
public readonly BeatmapInfo Beatmap ;
public readonly CancellationToken CancellationToken ;
public BindableStarDifficulty ( BeatmapInfo beatmap , CancellationToken cancellationToken )
{
Beatmap = beatmap ;
CancellationToken = cancellationToken ;
}
}
}
2020-07-16 11:38:33 +00:00
}