1
0
mirror of https://github.com/ppy/osu.git synced 2024-11-11 11:07:52 +08:00

Add support for bindable retrieval

This commit is contained in:
smoogipoo 2020-07-21 23:13:04 +09:00
parent 599a15edb8
commit 107b5ca4f2
4 changed files with 208 additions and 67 deletions

View File

@ -9,7 +9,9 @@ using System.Threading;
using System.Threading.Tasks;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics.Containers;
using osu.Framework.Lists;
using osu.Framework.Threading;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
@ -21,32 +23,154 @@ namespace osu.Game.Beatmaps
// 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;
// A cache that keeps references to BeatmapInfos for 60sec.
private readonly TimedExpiryCache<DifficultyCacheLookup, StarDifficulty> difficultyCache = new TimedExpiryCache<DifficultyCacheLookup, StarDifficulty> { ExpiryTime = 60000 };
public BeatmapDifficultyManager(BeatmapManager beatmapManager)
// 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()
{
this.beatmapManager = beatmapManager;
base.LoadComplete();
currentRuleset.BindValueChanged(_ => updateTrackedBindables());
currentMods.BindValueChanged(_ => updateTrackedBindables(), true);
}
public async Task<double> GetDifficultyAsync([NotNull] BeatmapInfo beatmapInfo, [CanBeNull] RulesetInfo rulesetInfo = null, [CanBeNull] IReadOnlyList<Mod> mods = null,
CancellationToken cancellationToken = default)
/// <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)
{
var bindable = createBindable(beatmapInfo, currentRuleset.Value, currentMods.Value, cancellationToken);
trackedBindables.Add(bindable);
return bindable;
}
/// <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)
{
if (tryGetGetExisting(beatmapInfo, rulesetInfo, mods, out var existing, out var key))
return existing;
return await Task.Factory.StartNew(() => getDifficulty(key), cancellationToken, TaskCreationOptions.HideScheduler | TaskCreationOptions.RunContinuationsAsynchronously, updateScheduler);
return await Task.Factory.StartNew(() => computeDifficulty(key), cancellationToken, TaskCreationOptions.HideScheduler | TaskCreationOptions.RunContinuationsAsynchronously, updateScheduler);
}
public double GetDifficulty([NotNull] BeatmapInfo beatmapInfo, [CanBeNull] RulesetInfo rulesetInfo = null, [CanBeNull] IReadOnlyList<Mod> mods = null)
/// <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)
{
if (tryGetGetExisting(beatmapInfo, rulesetInfo, mods, out var existing, out var key))
return existing;
return getDifficulty(key);
return computeDifficulty(key);
}
private double getDifficulty(in DifficultyCacheLookup key)
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>
/// <returns>The <see cref="StarDifficulty"/>.</returns>
private StarDifficulty computeDifficulty(in DifficultyCacheLookup key)
{
try
{
@ -56,13 +180,17 @@ namespace osu.Game.Beatmaps
var calculator = ruleset.CreateDifficultyCalculator(beatmapManager.GetWorkingBeatmap(key.BeatmapInfo));
var attributes = calculator.Calculate(key.Mods);
difficultyCache.Add(key, attributes.StarRating);
return attributes.StarRating;
var difficulty = new StarDifficulty(attributes.StarRating);
difficultyCache.Add(key, difficulty);
return difficulty;
}
catch
{
difficultyCache.Add(key, 0);
return 0;
var difficulty = new StarDifficulty(0);
difficultyCache.Add(key, difficulty);
return difficulty;
}
}
@ -73,9 +201,9 @@ namespace osu.Game.Beatmaps
/// <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>
/// <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>
/// <returns>Whether an existing difficulty was found.</returns>
private bool tryGetGetExisting(BeatmapInfo beatmapInfo, RulesetInfo rulesetInfo, IReadOnlyList<Mod> mods, out double existingDifficulty, out DifficultyCacheLookup key)
private bool tryGetGetExisting(BeatmapInfo beatmapInfo, RulesetInfo rulesetInfo, IReadOnlyList<Mod> mods, out StarDifficulty 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;
@ -83,7 +211,7 @@ namespace osu.Game.Beatmaps
// Difficulty can only be computed if the beatmap is locally available.
if (beatmapInfo.ID == 0)
{
existingDifficulty = 0;
existingDifficulty = new StarDifficulty(0);
key = default;
return true;
@ -122,5 +250,29 @@ namespace osu.Game.Beatmaps
return hashCode.ToHashCode();
}
}
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...)
}
}
}

View File

@ -199,7 +199,10 @@ namespace osu.Game
ScoreManager.Undelete(getBeatmapScores(item), true);
});
dependencies.Cache(new BeatmapDifficultyManager(BeatmapManager));
var difficultyManager = new BeatmapDifficultyManager();
dependencies.Cache(difficultyManager);
AddInternal(difficultyManager);
dependencies.Cache(KeyBindingStore = new KeyBindingStore(contextFactory, RulesetStore));
dependencies.Cache(SettingsStore = new SettingsStore(contextFactory));
dependencies.Cache(RulesetConfigCache = new RulesetConfigCache(SettingsStore));

View File

@ -15,7 +15,6 @@ using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events;
using osu.Framework.Threading;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Drawables;
using osu.Game.Graphics;
@ -23,8 +22,6 @@ using osu.Game.Graphics.Backgrounds;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osuTK;
using osuTK.Graphics;
@ -46,15 +43,12 @@ namespace osu.Game.Screens.Select.Carousel
[Resolved(CanBeNull = true)]
private BeatmapSetOverlay beatmapOverlay { get; set; }
[Resolved]
private IBindable<RulesetInfo> ruleset { get; set; }
[Resolved]
private IBindable<IReadOnlyList<Mod>> mods { get; set; }
[Resolved]
private BeatmapDifficultyManager difficultyManager { get; set; }
private IBindable<StarDifficulty> starDifficultyBindable;
private CancellationTokenSource starDifficultyCancellationSource;
public DrawableCarouselBeatmap(CarouselBeatmap panel)
: base(panel)
{
@ -160,36 +154,6 @@ namespace osu.Game.Screens.Select.Carousel
}
}
};
ruleset.BindValueChanged(_ => refreshStarCounter());
mods.BindValueChanged(_ => refreshStarCounter(), true);
}
private ScheduledDelegate scheduledRefresh;
private CancellationTokenSource cancellationSource;
private void refreshStarCounter()
{
scheduledRefresh?.Cancel();
scheduledRefresh = null;
cancellationSource?.Cancel();
cancellationSource = null;
// Only want to run the calculation when we become visible.
scheduledRefresh = Schedule(() =>
{
var ourSource = cancellationSource = new CancellationTokenSource();
difficultyManager.GetDifficultyAsync(beatmap, ruleset.Value, mods.Value, ourSource.Token).ContinueWith(t =>
{
// We're currently on a random threadpool thread which we must exit.
Schedule(() =>
{
if (!ourSource.IsCancellationRequested)
starCounter.Current = (float)t.Result;
});
}, ourSource.Token);
});
}
protected override void Selected()
@ -224,6 +188,17 @@ namespace osu.Game.Screens.Select.Carousel
if (Item.State.Value != CarouselItemState.Collapsed && Alpha == 0)
starCounter.ReplayAnimation();
if (Item.State.Value == CarouselItemState.Collapsed)
starDifficultyCancellationSource?.Cancel();
else
{
starDifficultyCancellationSource?.Cancel();
// We've potentially cancelled the computation above so a new bindable is required.
starDifficultyBindable = difficultyManager.GetTrackedBindable(beatmap, (starDifficultyCancellationSource = new CancellationTokenSource()).Token);
starDifficultyBindable.BindValueChanged(d => starCounter.Current = (float)d.NewValue.Stars, true);
}
base.ApplyState();
}
@ -248,5 +223,11 @@ namespace osu.Game.Screens.Select.Carousel
return items.ToArray();
}
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
starDifficultyCancellationSource?.Cancel();
}
}
}

View File

@ -15,7 +15,6 @@ using System.Collections.Generic;
using osu.Game.Rulesets.Mods;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using osu.Framework.Threading;
using osu.Framework.Utils;
using osu.Game.Configuration;
@ -149,6 +148,8 @@ namespace osu.Game.Screens.Select.Details
updateStarDifficulty();
}
private IBindable<StarDifficulty> normalStarDifficulty;
private IBindable<StarDifficulty> moddedStarDifficulty;
private CancellationTokenSource starDifficultyCancellationSource;
private void updateStarDifficulty()
@ -160,15 +161,19 @@ namespace osu.Game.Screens.Select.Details
var ourSource = starDifficultyCancellationSource = new CancellationTokenSource();
Task.WhenAll(difficultyManager.GetDifficultyAsync(Beatmap, ruleset.Value, cancellationToken: ourSource.Token),
difficultyManager.GetDifficultyAsync(Beatmap, ruleset.Value, mods.Value, ourSource.Token)).ContinueWith(t =>
{
Schedule(() =>
{
if (!ourSource.IsCancellationRequested)
starDifficulty.Value = ((float)t.Result[0], (float)t.Result[1]);
});
}, ourSource.Token);
normalStarDifficulty = difficultyManager.GetUntrackedBindable(Beatmap, ruleset.Value, cancellationToken: ourSource.Token);
moddedStarDifficulty = difficultyManager.GetUntrackedBindable(Beatmap, ruleset.Value, mods.Value, ourSource.Token);
normalStarDifficulty.BindValueChanged(_ => updateDisplay());
moddedStarDifficulty.BindValueChanged(_ => updateDisplay(), true);
void updateDisplay() => starDifficulty.Value = ((float)normalStarDifficulty.Value.Stars, (float)moddedStarDifficulty.Value.Stars);
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
starDifficultyCancellationSource?.Cancel();
}
public class StatisticRow : Container, IHasAccentColour