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 ;
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 ;
using osu.Framework.Lists ;
2020-10-12 15:31:42 +08:00
using osu.Framework.Logging ;
2020-07-16 19:38:33 +08:00
using osu.Framework.Threading ;
2020-10-11 00:15:52 +08:00
using osu.Framework.Utils ;
2020-11-06 12:26:18 +08:00
using osu.Game.Database ;
2020-07-16 19:38:33 +08:00
using osu.Game.Rulesets ;
using osu.Game.Rulesets.Mods ;
2020-10-12 15:31:42 +08:00
using osu.Game.Rulesets.UI ;
2020-07-16 19:38:33 +08:00
namespace osu.Game.Beatmaps
{
2020-11-06 12:14:23 +08: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 12:26:18 +08:00
public class BeatmapDifficultyCache : MemoryCachingComponent < BeatmapDifficultyCache . DifficultyCacheLookup , StarDifficulty >
2020-07-16 19:38:33 +08:00
{
// Too many simultaneous updates can lead to stutters. One thread seems to work fine for song select display purposes.
2020-11-06 12:14:23 +08:00
private readonly ThreadedTaskScheduler updateScheduler = new ThreadedTaskScheduler ( 1 , nameof ( BeatmapDifficultyCache ) ) ;
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>
2020-07-24 12:52:43 +08:00
/// Retrieves a bindable containing the star difficulty of a <see cref="BeatmapInfo"/> that follows the currently-selected ruleset and mods.
2020-07-21 22:13:04 +08: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 12:38:53 +08:00
/// <returns>A bindable that is updated to contain the star difficulty when it becomes available.</returns>
2020-07-24 12:52:43 +08:00
public IBindable < StarDifficulty > GetBindableDifficulty ( [ NotNull ] BeatmapInfo beatmapInfo , CancellationToken cancellationToken = default )
{
var bindable = createBindable ( beatmapInfo , currentRuleset . Value , currentMods . Value , cancellationToken ) ;
trackedBindables . Add ( bindable ) ;
return bindable ;
}
2020-07-21 22:13:04 +08:00
/// <summary>
2020-07-24 12:52:43 +08: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 22:13:04 +08:00
/// </summary>
/// <remarks>
2020-07-24 12:52:43 +08:00
/// The bindable will not update to follow the currently-selected ruleset and mods.
2020-07-21 22:13:04 +08:00
/// </remarks>
/// <param name="beatmapInfo">The <see cref="BeatmapInfo"/> to get the difficulty of.</param>
2020-07-24 12:54:47 +08: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 22:13:04 +08: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 12:52:43 +08:00
/// <returns>A bindable that is updated to contain the star difficulty when it becomes available.</returns>
2020-07-24 12:54:47 +08:00
public IBindable < StarDifficulty > GetBindableDifficulty ( [ NotNull ] BeatmapInfo beatmapInfo , [ CanBeNull ] RulesetInfo rulesetInfo , [ CanBeNull ] IEnumerable < Mod > mods ,
2020-07-24 12:52:43 +08:00
CancellationToken cancellationToken = default )
= > createBindable ( beatmapInfo , rulesetInfo , mods , cancellationToken ) ;
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>
2020-11-06 13:31:21 +08:00
public Task < StarDifficulty > GetDifficultyAsync ( [ NotNull ] BeatmapInfo beatmapInfo , [ CanBeNull ] RulesetInfo rulesetInfo = null , [ CanBeNull ] IEnumerable < Mod > mods = null , CancellationToken cancellationToken = default )
2020-07-16 19:38:33 +08:00
{
2020-11-06 13:31:21 +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-16 19:38:33 +08:00
2020-11-06 13:31:21 +08:00
// Difficulty can only be computed if the beatmap and ruleset are locally available.
if ( beatmapInfo . ID = = 0 | | rulesetInfo . ID = = null )
2020-08-28 21:08:28 +08:00
{
2020-11-06 13:31:21 +08: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 21:08:28 +08:00
return existing ;
2020-11-06 13:31:21 +08:00
return computeDifficulty ( lookup ) ;
} , token , TaskCreationOptions . HideScheduler | TaskCreationOptions . RunContinuationsAsynchronously , updateScheduler ) ;
2020-07-16 20:07:14 +08:00
}
2020-07-16 19:38:33 +08:00
2020-10-01 19:50:47 +08: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-11 00:15:52 +08:00
if ( Precision . AlmostBigger ( starRating , 6.5 , 0.005 ) )
return DifficultyRating . ExpertPlus ;
2020-10-01 19:50:47 +08:00
2020-10-11 00:15:52 +08: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 19:50:47 +08:00
}
2020-07-21 22:13:04 +08:00
private CancellationTokenSource trackedUpdateCancellationSource ;
2020-07-28 15:52:07 +08:00
private readonly List < CancellationTokenSource > linkedCancellationSources = new List < CancellationTokenSource > ( ) ;
2020-07-21 22:13:04 +08:00
/// <summary>
/// Updates all tracked <see cref="BindableStarDifficulty"/> using the current ruleset and mods.
/// </summary>
private void updateTrackedBindables ( )
{
2020-07-28 15:52:07 +08:00
cancelTrackedBindableUpdate ( ) ;
2020-07-21 22:13:04 +08:00
trackedUpdateCancellationSource = new CancellationTokenSource ( ) ;
foreach ( var b in trackedBindables )
{
2020-07-28 15:52:07 +08:00
var linkedSource = CancellationTokenSource . CreateLinkedTokenSource ( trackedUpdateCancellationSource . Token , b . CancellationToken ) ;
linkedCancellationSources . Add ( linkedSource ) ;
updateBindable ( b , currentRuleset . Value , currentMods . Value , linkedSource . Token ) ;
}
}
/// <summary>
/// Cancels the existing update of all tracked <see cref="BindableStarDifficulty"/> via <see cref="updateTrackedBindables"/>.
/// </summary>
private void cancelTrackedBindableUpdate ( )
{
trackedUpdateCancellationSource ? . Cancel ( ) ;
trackedUpdateCancellationSource = null ;
2020-07-21 22:13:04 +08:00
2020-07-29 10:30:25 +08:00
if ( linkedCancellationSources ! = null )
{
foreach ( var c in linkedCancellationSources )
c . Dispose ( ) ;
2020-07-28 15:52:07 +08:00
2020-07-29 10:30:25 +08:00
linkedCancellationSources . Clear ( ) ;
}
2020-07-21 22:13:04 +08:00
}
2020-07-28 15:52:19 +08: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 22:13:04 +08: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 12:40:01 +08:00
private void updateBindable ( [ NotNull ] BindableStarDifficulty bindable , [ CanBeNull ] RulesetInfo rulesetInfo , [ CanBeNull ] IEnumerable < Mod > mods , CancellationToken cancellationToken = default )
2020-07-21 22:13:04 +08:00
{
2020-11-06 13:31:21 +08:00
GetAsync ( new DifficultyCacheLookup ( bindable . Beatmap , rulesetInfo , mods ) , cancellationToken )
. ContinueWith ( t = >
2020-07-21 22:13:04 +08:00
{
2020-11-06 13:31:21 +08: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 22:13:04 +08: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 13:31:21 +08:00
private StarDifficulty computeDifficulty ( in DifficultyCacheLookup key )
2020-07-16 20:07:14 +08:00
{
2020-07-22 11:48:12 +08:00
// In the case that the user hasn't given us a ruleset, use the beatmap's default ruleset.
2020-11-06 13:31:21 +08:00
var beatmapInfo = key . Beatmap ;
var rulesetInfo = key . Ruleset ;
2020-07-22 11:48:12 +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-11-06 13:31:21 +08:00
var calculator = ruleset . CreateDifficultyCalculator ( beatmapManager . GetWorkingBeatmap ( key . Beatmap ) ) ;
var attributes = calculator . Calculate ( key . OrderedMods ) ;
2020-07-16 19:38:33 +08:00
2020-11-06 13:31:21 +08:00
return new StarDifficulty ( attributes ) ;
2020-07-16 19:38:33 +08:00
}
2020-10-12 15:31:42 +08: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 13:31:21 +08:00
return new StarDifficulty ( ) ;
2020-10-12 15:31:42 +08:00
}
2020-11-06 13:31:21 +08:00
return GetAsync ( new DifficultyCacheLookup ( key . Beatmap , key . Beatmap . Ruleset , key . OrderedMods ) ) . Result ;
2020-10-12 15:31:42 +08:00
}
2020-07-16 19:38:33 +08:00
catch
{
2020-11-06 13:31:21 +08:00
return new StarDifficulty ( ) ;
2020-07-16 20:07:14 +08:00
}
}
2020-07-28 15:52:07 +08:00
protected override void Dispose ( bool isDisposing )
{
base . Dispose ( isDisposing ) ;
2020-07-28 16:23:35 +08:00
2020-07-28 15:52:07 +08:00
cancelTrackedBindableUpdate ( ) ;
2020-07-29 10:30:25 +08:00
updateScheduler ? . Dispose ( ) ;
2020-07-28 15:52:07 +08:00
}
2020-08-28 21:12:17 +08:00
public readonly struct DifficultyCacheLookup : IEquatable < DifficultyCacheLookup >
2020-07-16 19:38:33 +08:00
{
2020-11-06 13:31:21 +08:00
public readonly BeatmapInfo Beatmap ;
public readonly RulesetInfo Ruleset ;
public readonly Mod [ ] OrderedMods ;
2020-07-16 19:38:33 +08:00
2020-11-06 13:31:21 +08:00
public DifficultyCacheLookup ( [ NotNull ] BeatmapInfo beatmap , [ NotNull ] RulesetInfo ruleset , IEnumerable < Mod > mods )
2020-07-16 19:38:33 +08:00
{
2020-11-06 13:31:21 +08:00
Beatmap = beatmap ;
Ruleset = ruleset ;
OrderedMods = mods ? . OrderBy ( m = > m . Acronym ) . ToArray ( ) ? ? Array . Empty < Mod > ( ) ;
2020-07-16 19:38:33 +08:00
}
public bool Equals ( DifficultyCacheLookup other )
2020-11-06 13:31:21 +08: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 19:38:33 +08:00
public override int GetHashCode ( )
{
var hashCode = new HashCode ( ) ;
2020-11-06 13:31:21 +08:00
hashCode . Add ( Beatmap . ID ) ;
hashCode . Add ( Ruleset . ID ) ;
foreach ( var mod in OrderedMods )
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 ;
}
}
}
2020-07-16 19:38:33 +08:00
}