diff --git a/osu.Game/Beatmaps/BeatmapDifficultyCache.cs b/osu.Game/Beatmaps/BeatmapDifficultyCache.cs
index d6b4063916..6181102736 100644
--- a/osu.Game/Beatmaps/BeatmapDifficultyCache.cs
+++ b/osu.Game/Beatmaps/BeatmapDifficultyCache.cs
@@ -137,10 +137,15 @@ namespace osu.Game.Beatmaps
Value = new StarDifficulty(beatmapInfo.StarRating, 0)
};
- updateBindable(bindable, currentRuleset.Value, currentMods.Value, cancellationToken, computationDelay);
-
lock (bindableUpdateLock)
+ {
+ var linkedSource = CancellationTokenSource.CreateLinkedTokenSource(trackedUpdateCancellationSource.Token, cancellationToken);
+ linkedCancellationSources.Add(linkedSource);
+
+ updateBindable(bindable, currentRuleset.Value, currentMods.Value, linkedSource, computationDelay);
+
trackedBindables.Add(bindable);
+ }
return bindable;
}
@@ -212,7 +217,7 @@ namespace osu.Game.Beatmaps
var linkedSource = CancellationTokenSource.CreateLinkedTokenSource(trackedUpdateCancellationSource.Token, b.CancellationToken);
linkedCancellationSources.Add(linkedSource);
- updateBindable(b, currentRuleset.Value, currentMods.Value, linkedSource.Token);
+ updateBindable(b, currentRuleset.Value, currentMods.Value, linkedSource);
}
}
}
@@ -243,27 +248,45 @@ namespace osu.Game.Beatmaps
/// The to update.
/// The to update with.
/// The s to update with.
- /// A token that may be used to cancel this update.
+ ///
+ /// A cancellation token source that may be used to cancel this update.
+ /// This token will be cancelled in one of two scenarios:
+ ///
+ /// - The owner of the bindable has requested the cancellation.
+ /// - An call has been issued, and as such ongoing calculations must be aborted to avoid stale values being potentially written to bindables.
+ ///
+ ///
/// In the case a cached lookup was not possible, a value in milliseconds of to wait until performing potentially intensive lookup.
- private void updateBindable(BindableStarDifficulty bindable, IRulesetInfo? rulesetInfo, IEnumerable? mods, CancellationToken cancellationToken = default, int computationDelay = 0)
+ private void updateBindable(BindableStarDifficulty bindable, IRulesetInfo? rulesetInfo, IEnumerable? mods, CancellationTokenSource linkedCancellationTokenSource, int computationDelay = 0)
{
// GetDifficultyAsync will fall back to existing data from IBeatmapInfo if not locally available
// (contrary to GetAsync)
- GetDifficultyAsync(bindable.BeatmapInfo, rulesetInfo, mods, cancellationToken, computationDelay)
+ GetDifficultyAsync(bindable.BeatmapInfo, rulesetInfo, mods, linkedCancellationTokenSource.Token, computationDelay)
.ContinueWith(task =>
- {
- // 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)
- return;
+ // 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 (!linkedCancellationTokenSource.IsCancellationRequested)
+ {
+ StarDifficulty? starDifficulty = task.GetResultSafely();
- StarDifficulty? starDifficulty = task.GetResultSafely();
+ if (starDifficulty != null)
+ bindable.Value = starDifficulty.Value;
+ }
- if (starDifficulty != null)
- bindable.Value = starDifficulty.Value;
- });
- }, cancellationToken);
+ // Once the linked cancellation token source is of no remaining use to anybody, clean it up.
+ lock (bindableUpdateLock)
+ {
+ linkedCancellationSources.Remove(linkedCancellationTokenSource);
+ linkedCancellationTokenSource.Dispose();
+ }
+ });
+ },
+ // This continuation MUST run even if the antecedent `GetDifficultyAsync()` call was canceled in order to clean up `linkedCancellationTokenSource`.
+ // Due to this, `ContinueWith()` CANNOT accept `linkedCancellationTokenSource.Token` here, because if it did, then in an event of a cancellation,
+ // the continuation would never be scheduled for execution.
+ CancellationToken.None);
}
///