diff --git a/osu.Game/Online/Leaderboards/Leaderboard.cs b/osu.Game/Online/Leaderboards/Leaderboard.cs index eeb814b4cb..0d430f4903 100644 --- a/osu.Game/Online/Leaderboards/Leaderboard.cs +++ b/osu.Game/Online/Leaderboards/Leaderboard.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Threading; using JetBrains.Annotations; @@ -13,7 +14,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; -using osu.Framework.Threading; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Cursor; using osu.Game.Graphics.UserInterface; @@ -44,10 +44,9 @@ namespace osu.Game.Online.Leaderboards private readonly LoadingSpinner loading; - private CancellationTokenSource showScoresCancellationSource; + private CancellationTokenSource currentFetchCancellationSource; - private APIRequest getScoresRequest; - private ScheduledDelegate getScoresRequestCallback; + private APIRequest fetchScoresRequest; [Resolved(CanBeNull = true)] private IAPIProvider api { get; set; } @@ -62,7 +61,6 @@ namespace osu.Game.Online.Leaderboards protected set { scores = value; - updateScoresDrawables(); } } @@ -177,9 +175,10 @@ namespace osu.Game.Online.Leaderboards /// /// Performs a fetch/refresh of scores to be displayed. /// + /// /// An responsible for the fetch operation. This will be queued and performed automatically. [CanBeNull] - protected abstract APIRequest FetchScores(); + protected abstract APIRequest FetchScores(CancellationToken cancellationToken); protected abstract LeaderboardScore CreateDrawableScore(TScoreInfo model, int index); @@ -187,37 +186,36 @@ namespace osu.Game.Online.Leaderboards private void refetchScores() { - cancelPendingWork(); + Reset(); PlaceholderState = PlaceholderState.Retrieving; loading.Show(); - getScoresRequest = FetchScores(); + currentFetchCancellationSource = new CancellationTokenSource(); - if (getScoresRequest == null) + fetchScoresRequest = FetchScores(currentFetchCancellationSource.Token); + + if (fetchScoresRequest == null) return; - getScoresRequest.Failure += e => getScoresRequestCallback = Schedule(() => + fetchScoresRequest.Failure += e => Schedule(() => { - if (e is OperationCanceledException) + if (e is OperationCanceledException || currentFetchCancellationSource.IsCancellationRequested) return; PlaceholderState = PlaceholderState.NetworkFailure; }); - api?.Queue(getScoresRequest); + api?.Queue(fetchScoresRequest); } private void cancelPendingWork() { - showScoresCancellationSource?.Cancel(); - showScoresCancellationSource = null; + currentFetchCancellationSource?.Cancel(); + currentFetchCancellationSource = null; - getScoresRequest?.Cancel(); - getScoresRequest = null; - - getScoresRequestCallback?.Cancel(); - getScoresRequestCallback = null; + fetchScoresRequest?.Cancel(); + fetchScoresRequest = null; } #region Placeholder handling @@ -279,8 +277,6 @@ namespace osu.Game.Online.Leaderboards scrollFlow?.FadeOut(fade_duration, Easing.OutQuint).Expire(); scrollFlow = null; - showScoresCancellationSource?.Cancel(); - if (scores?.Any() != true) { loading.Hide(); @@ -288,6 +284,8 @@ namespace osu.Game.Online.Leaderboards return; } + Debug.Assert(!currentFetchCancellationSource.IsCancellationRequested); + // ensure placeholder is hidden when displaying scores PlaceholderState = PlaceholderState.Successful; @@ -315,7 +313,7 @@ namespace osu.Game.Online.Leaderboards scrollContainer.ScrollToStart(false); loading.Hide(); - }, (showScoresCancellationSource = new CancellationTokenSource()).Token); + }, currentFetchCancellationSource.Token); }, false); private void replacePlaceholder(Placeholder placeholder) diff --git a/osu.Game/Screens/OnlinePlay/Match/Components/MatchLeaderboard.cs b/osu.Game/Screens/OnlinePlay/Match/Components/MatchLeaderboard.cs index 5b60f64160..1945899a11 100644 --- a/osu.Game/Screens/OnlinePlay/Match/Components/MatchLeaderboard.cs +++ b/osu.Game/Screens/OnlinePlay/Match/Components/MatchLeaderboard.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Threading; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Game.Online.API; @@ -30,7 +31,7 @@ namespace osu.Game.Screens.OnlinePlay.Match.Components protected override bool IsOnlineScope => true; - protected override APIRequest FetchScores() + protected override APIRequest FetchScores(CancellationToken cancellationToken) { if (roomId.Value == null) return null; @@ -39,6 +40,9 @@ namespace osu.Game.Screens.OnlinePlay.Match.Components req.Success += r => { + if (cancellationToken.IsCancellationRequested) + return; + Scores = r.Leaderboard; TopScore = r.UserScore; }; diff --git a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs index 898b894541..388395c9f9 100644 --- a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs +++ b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs @@ -108,13 +108,8 @@ namespace osu.Game.Screens.Select.Leaderboards protected override bool IsOnlineScope => Scope != BeatmapLeaderboardScope.Local; - private CancellationTokenSource loadCancellationSource; - - protected override APIRequest FetchScores() + protected override APIRequest FetchScores(CancellationToken cancellationToken) { - loadCancellationSource?.Cancel(); - loadCancellationSource = new CancellationTokenSource(); - var fetchBeatmapInfo = BeatmapInfo; if (fetchBeatmapInfo == null) @@ -125,7 +120,7 @@ namespace osu.Game.Screens.Select.Leaderboards if (Scope == BeatmapLeaderboardScope.Local) { - subscribeToLocalScores(); + subscribeToLocalScores(cancellationToken); return null; } @@ -160,10 +155,10 @@ namespace osu.Game.Screens.Select.Leaderboards req.Success += r => { - scoreManager.OrderByTotalScoreAsync(r.Scores.Select(s => s.CreateScoreInfo(rulesets, fetchBeatmapInfo)).ToArray(), loadCancellationSource.Token) + scoreManager.OrderByTotalScoreAsync(r.Scores.Select(s => s.CreateScoreInfo(rulesets, fetchBeatmapInfo)).ToArray(), cancellationToken) .ContinueWith(task => Schedule(() => { - if (loadCancellationSource.IsCancellationRequested) + if (cancellationToken.IsCancellationRequested) return; Scores = task.GetResultSafely(); @@ -184,7 +179,7 @@ namespace osu.Game.Screens.Select.Leaderboards Action = () => ScoreSelected?.Invoke(model) }; - private void subscribeToLocalScores() + private void subscribeToLocalScores(CancellationToken cancellationToken) { scoreSubscription?.Dispose(); scoreSubscription = null; @@ -197,35 +192,35 @@ namespace osu.Game.Screens.Select.Leaderboards + $" AND {nameof(ScoreInfo.Ruleset)}.{nameof(RulesetInfo.ShortName)} == $1" + $" AND {nameof(ScoreInfo.DeletePending)} == false" , beatmapInfo.ID, ruleset.Value.ShortName), localScoresChanged); - } - private void localScoresChanged(IRealmCollection sender, ChangeSet changes, Exception exception) - { - if (IsOnlineScope) - return; - - var scores = sender.AsEnumerable(); - - if (filterMods && !mods.Value.Any()) + void localScoresChanged(IRealmCollection sender, ChangeSet changes, Exception exception) { - // we need to filter out all scores that have any mods to get all local nomod scores - scores = scores.Where(s => !s.Mods.Any()); - } - else if (filterMods) - { - // otherwise find all the scores that have *any* of the currently selected mods (similar to how web applies mod filters) - // we're creating and using a string list representation of selected mods so that it can be translated into the DB query itself - var selectedMods = mods.Value.Select(m => m.Acronym); - scores = scores.Where(s => s.Mods.Any(m => selectedMods.Contains(m.Acronym))); - } + if (IsOnlineScope || cancellationToken.IsCancellationRequested) + return; - scores = scores.Detach(); + var scores = sender.AsEnumerable(); - scoreManager.OrderByTotalScoreAsync(scores.ToArray(), loadCancellationSource.Token) - .ContinueWith(ordered => - { - Scores = ordered.GetResultSafely(); - }, TaskContinuationOptions.OnlyOnRanToCompletion); + if (filterMods && !mods.Value.Any()) + { + // we need to filter out all scores that have any mods to get all local nomod scores + scores = scores.Where(s => !s.Mods.Any()); + } + else if (filterMods) + { + // otherwise find all the scores that have *any* of the currently selected mods (similar to how web applies mod filters) + // we're creating and using a string list representation of selected mods so that it can be translated into the DB query itself + var selectedMods = mods.Value.Select(m => m.Acronym); + scores = scores.Where(s => s.Mods.Any(m => selectedMods.Contains(m.Acronym))); + } + + scores = scores.Detach(); + + scoreManager.OrderByTotalScoreAsync(scores.ToArray(), cancellationToken) + .ContinueWith(ordered => + { + Scores = ordered.GetResultSafely(); + }, TaskContinuationOptions.OnlyOnRanToCompletion); + } } protected override void Dispose(bool isDisposing)