diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs index e31be1d51a..48230ff9e9 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; @@ -100,7 +101,7 @@ namespace osu.Game.Tests.Visual.SongSelect public void TestGlobalScoresDisplay() { AddStep(@"Set scope", () => leaderboard.Scope = BeatmapLeaderboardScope.Global); - AddStep(@"New Scores", () => leaderboard.Scores = generateSampleScores(null)); + AddStep(@"New Scores", () => leaderboard.SetScores(generateSampleScores(new BeatmapInfo()))); } [Test] @@ -113,24 +114,18 @@ namespace osu.Game.Tests.Visual.SongSelect [Test] public void TestPlaceholderStates() { - AddStep(@"Empty Scores", () => leaderboard.SetRetrievalState(PlaceholderState.NoScores)); - AddStep(@"Network failure", () => leaderboard.SetRetrievalState(PlaceholderState.NetworkFailure)); - AddStep(@"No supporter", () => leaderboard.SetRetrievalState(PlaceholderState.NotSupporter)); - AddStep(@"Not logged in", () => leaderboard.SetRetrievalState(PlaceholderState.NotLoggedIn)); - AddStep(@"Unavailable", () => leaderboard.SetRetrievalState(PlaceholderState.Unavailable)); - AddStep(@"None selected", () => leaderboard.SetRetrievalState(PlaceholderState.NoneSelected)); - } + AddStep("ensure no scores displayed", () => leaderboard.SetScores(null)); - [Test] - public void TestBeatmapStates() - { - foreach (BeatmapOnlineStatus status in Enum.GetValues(typeof(BeatmapOnlineStatus))) - AddStep($"{status} beatmap", () => showBeatmapWithStatus(status)); + AddStep(@"Network failure", () => leaderboard.SetErrorState(LeaderboardState.NetworkFailure)); + AddStep(@"No supporter", () => leaderboard.SetErrorState(LeaderboardState.NotSupporter)); + AddStep(@"Not logged in", () => leaderboard.SetErrorState(LeaderboardState.NotLoggedIn)); + AddStep(@"Unavailable", () => leaderboard.SetErrorState(LeaderboardState.Unavailable)); + AddStep(@"None selected", () => leaderboard.SetErrorState(LeaderboardState.NoneSelected)); } private void showPersonalBestWithNullPosition() { - leaderboard.TopScore = new ScoreInfo + leaderboard.SetScores(leaderboard.Scores, new ScoreInfo { Rank = ScoreRank.XH, Accuracy = 1, @@ -148,12 +143,12 @@ namespace osu.Game.Tests.Visual.SongSelect FlagName = @"ES", }, }, - }; + }); } private void showPersonalBest() { - leaderboard.TopScore = new ScoreInfo + leaderboard.SetScores(leaderboard.Scores, new ScoreInfo { Position = 999, Rank = ScoreRank.XH, @@ -172,7 +167,7 @@ namespace osu.Game.Tests.Visual.SongSelect FlagName = @"ES", }, }, - }; + }); } private void loadMoreScores(Func beatmapInfo) @@ -407,21 +402,10 @@ namespace osu.Game.Tests.Visual.SongSelect }; } - private void showBeatmapWithStatus(BeatmapOnlineStatus status) - { - leaderboard.BeatmapInfo = new BeatmapInfo - { - OnlineID = 1113057, - Status = status, - }; - } - private class FailableLeaderboard : BeatmapLeaderboard { - public void SetRetrievalState(PlaceholderState state) - { - PlaceholderState = state; - } + public new void SetErrorState(LeaderboardState state) => base.SetErrorState(state); + public new void SetScores(IEnumerable scores, ScoreInfo userScore = default) => base.SetScores(scores, userScore); } } } diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs index 4826d2fb33..da4cf9c6e3 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs @@ -128,21 +128,16 @@ namespace osu.Game.Tests.Visual.UserInterface scoreManager.Undelete(r.All().Where(s => s.DeletePending).ToList()); }); - leaderboard.Scores = null; - leaderboard.FinishTransforms(true); // After setting scores, we may be waiting for transforms to expire drawables - leaderboard.BeatmapInfo = beatmapInfo; - leaderboard.RefreshScores(); // Required in the case that the beatmap hasn't changed + leaderboard.RefetchScores(); // Required in the case that the beatmap hasn't changed }); [SetUpSteps] public void SetupSteps() { - // Ensure the leaderboard has finished async-loading drawables - AddUntilStep("wait for drawables", () => leaderboard.ChildrenOfType().Any()); - // Ensure the leaderboard items have finished showing up AddStep("finish transforms", () => leaderboard.FinishTransforms(true)); + AddUntilStep("wait for drawables", () => leaderboard.ChildrenOfType().Any()); } [Test] diff --git a/osu.Game/Online/Leaderboards/Leaderboard.cs b/osu.Game/Online/Leaderboards/Leaderboard.cs index 515cc6fd73..5dd3e46b4a 100644 --- a/osu.Game/Online/Leaderboards/Leaderboard.cs +++ b/osu.Game/Online/Leaderboards/Leaderboard.cs @@ -3,16 +3,18 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Threading; +using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Development; using osu.Framework.Extensions.Color4Extensions; 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; @@ -23,98 +25,48 @@ using osuTK.Graphics; namespace osu.Game.Online.Leaderboards { - public abstract class Leaderboard : Container + /// + /// A leaderboard which displays a scrolling list of top scores, along with a single "user best" + /// for the local user. + /// + /// The scope of the leaderboard (ie. global or local). + /// The score model class. + public abstract class Leaderboard : CompositeDrawable { + /// + /// The currently displayed scores. + /// + public IEnumerable Scores => scores; + + /// + /// Whether the current scope should refetch in response to changes in API connectivity state. + /// + protected abstract bool IsOnlineScope { get; } + private const double fade_duration = 300; private readonly OsuScrollContainer scrollContainer; private readonly Container placeholderContainer; - private readonly UserTopScoreContainer topScoreContainer; + private readonly UserTopScoreContainer userScoreContainer; - private FillFlowContainer scrollFlow; + private FillFlowContainer scoreFlowContainer; private readonly LoadingSpinner loading; - private ScheduledDelegate showScoresDelegate; - private CancellationTokenSource showScoresCancellationSource; + private CancellationTokenSource currentFetchCancellationSource; + private CancellationTokenSource currentScoresAsyncLoadCancellationSource; - private bool scoresLoadedOnce; + private APIRequest fetchScoresRequest; - private readonly Container content; + private LeaderboardState state; - protected override Container Content => content; + [Resolved(CanBeNull = true)] + private IAPIProvider api { get; set; } + + private readonly IBindable apiState = new Bindable(); private ICollection scores; - public ICollection Scores - { - get => scores; - set - { - scores = value; - - scoresLoadedOnce = true; - - scrollFlow?.FadeOut(fade_duration, Easing.OutQuint).Expire(); - scrollFlow = null; - - showScoresDelegate?.Cancel(); - showScoresCancellationSource?.Cancel(); - - if (scores == null || !scores.Any()) - { - loading.Hide(); - return; - } - - // ensure placeholder is hidden when displaying scores - PlaceholderState = PlaceholderState.Successful; - - var scoreFlow = CreateScoreFlow(); - scoreFlow.ChildrenEnumerable = scores.Select((s, index) => CreateDrawableScore(s, index + 1)); - - // schedule because we may not be loaded yet (LoadComponentAsync complains). - showScoresDelegate = Schedule(() => LoadComponentAsync(scoreFlow, _ => - { - scrollContainer.Add(scrollFlow = scoreFlow); - - int i = 0; - - foreach (var s in scrollFlow.Children) - { - using (s.BeginDelayedSequence(i++ * 50)) - s.Show(); - } - - scrollContainer.ScrollTo(0f, false); - loading.Hide(); - }, (showScoresCancellationSource = new CancellationTokenSource()).Token)); - } - } - - public TScoreInfo TopScore - { - get => topScoreContainer.Score.Value; - set - { - topScoreContainer.Score.Value = value; - - if (value == null) - topScoreContainer.Hide(); - else - topScoreContainer.Show(); - } - } - - protected virtual FillFlowContainer CreateScoreFlow() - => new FillFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Spacing = new Vector2(0f, 5f), - Padding = new MarginPadding { Top = 10, Bottom = 5 }, - }; - private TScope scope; public TScope Scope @@ -126,62 +78,7 @@ namespace osu.Game.Online.Leaderboards return; scope = value; - RefreshScores(); - } - } - - private PlaceholderState placeholderState; - - /// - /// Update the placeholder visibility. - /// Setting this to anything other than PlaceholderState.Successful will cancel all existing retrieval requests and hide scores. - /// - protected PlaceholderState PlaceholderState - { - get => placeholderState; - set - { - if (value != PlaceholderState.Successful) - { - Reset(); - } - - if (value == placeholderState) - return; - - switch (placeholderState = value) - { - case PlaceholderState.NetworkFailure: - replacePlaceholder(new ClickablePlaceholder(@"Couldn't fetch scores!", FontAwesome.Solid.Sync) - { - Action = RefreshScores - }); - break; - - case PlaceholderState.NoneSelected: - replacePlaceholder(new MessagePlaceholder(@"Please select a beatmap!")); - break; - - case PlaceholderState.Unavailable: - replacePlaceholder(new MessagePlaceholder(@"Leaderboards are not available for this beatmap!")); - break; - - case PlaceholderState.NoScores: - replacePlaceholder(new MessagePlaceholder(@"No records yet!")); - break; - - case PlaceholderState.NotLoggedIn: - replacePlaceholder(new LoginPlaceholder(@"Please sign in to view online leaderboards!")); - break; - - case PlaceholderState.NotSupporter: - replacePlaceholder(new MessagePlaceholder(@"Please invest in an osu!supporter tag to view this leaderboard!")); - break; - - default: - replacePlaceholder(null); - break; - } + RefetchScores(); } } @@ -213,12 +110,7 @@ namespace osu.Game.Online.Leaderboards }, new Drawable[] { - content = new Container - { - AutoSizeAxes = Axes.Y, - RelativeSizeAxes = Axes.X, - Child = topScoreContainer = new UserTopScoreContainer(CreateDrawableTopScore) - }, + userScoreContainer = new UserTopScoreContainer(CreateDrawableTopScore) }, }, }, @@ -231,120 +123,228 @@ namespace osu.Game.Online.Leaderboards }; } - protected virtual void Reset() + protected override void LoadComplete() { - getScoresRequest?.Cancel(); - getScoresRequest = null; - Scores = null; - } + base.LoadComplete(); - [Resolved(CanBeNull = true)] - private IAPIProvider api { get; set; } - - private ScheduledDelegate pendingUpdateScores; - - private readonly IBindable apiState = new Bindable(); - - [BackgroundDependencyLoader] - private void load() - { if (api != null) + { apiState.BindTo(api.State); + apiState.BindValueChanged(state => + { + switch (state.NewValue) + { + case APIState.Online: + case APIState.Offline: + if (IsOnlineScope) + RefetchScores(); - apiState.BindValueChanged(onlineStateChanged, true); + break; + } + }); + } + + RefetchScores(); } - private APIRequest getScoresRequest; - private ScheduledDelegate getScoresRequestCallback; + /// + /// Perform a full refetch of scores using current criteria. + /// + public void RefetchScores() => Scheduler.AddOnce(refetchScores); - protected abstract bool IsOnlineScope { get; } - - private void onlineStateChanged(ValueChangedEvent state) => Schedule(() => + /// + /// Call when a retrieval or display failure happened to show a relevant message to the user. + /// + /// The state to display. + protected void SetErrorState(LeaderboardState state) { - switch (state.NewValue) + switch (state) { - case APIState.Online: - case APIState.Offline: - if (IsOnlineScope) - RefreshScores(); - - break; + case LeaderboardState.NoScores: + case LeaderboardState.Retrieving: + case LeaderboardState.Success: + throw new InvalidOperationException($"State {state} cannot be set by a leaderboard implementation."); } - }); - public void RefreshScores() => Scheduler.AddOnce(UpdateScores); + Debug.Assert(scores?.Any() != true); - protected void UpdateScores() + setState(state); + } + + /// + /// Call when retrieved scores are ready to be displayed. + /// + /// The scores to display. + /// The user top score, if any. + protected void SetScores(IEnumerable scores, TScoreInfo userScore = default) { - // don't display any scores or placeholder until the first Scores_Set has been called. - // this avoids scope changes flickering a "no scores" placeholder before initialisation of song select is finished. - if (!scoresLoadedOnce) return; + this.scores = scores?.ToList(); + userScoreContainer.Score.Value = userScore; - getScoresRequest?.Cancel(); - getScoresRequest = null; + if (userScore == null) + userScoreContainer.Hide(); + else + userScoreContainer.Show(); - getScoresRequestCallback?.Cancel(); - getScoresRequestCallback = null; - - pendingUpdateScores?.Cancel(); - pendingUpdateScores = Schedule(() => - { - PlaceholderState = PlaceholderState.Retrieving; - loading.Show(); - - getScoresRequest = FetchScores(scores => getScoresRequestCallback = Schedule(() => - { - Scores = scores.ToArray(); - PlaceholderState = Scores.Any() ? PlaceholderState.Successful : PlaceholderState.NoScores; - })); - - if (getScoresRequest == null) - return; - - getScoresRequest.Failure += e => getScoresRequestCallback = Schedule(() => - { - if (e is OperationCanceledException) - return; - - PlaceholderState = PlaceholderState.NetworkFailure; - }); - - api?.Queue(getScoresRequest); - }); + Scheduler.Add(updateScoresDrawables, false); } /// /// Performs a fetch/refresh of scores to be displayed. /// - /// A callback which should be called when fetching is completed. Scheduling is not required. + /// /// An responsible for the fetch operation. This will be queued and performed automatically. - protected abstract APIRequest FetchScores(Action> scoresCallback); + [CanBeNull] + protected abstract APIRequest FetchScores(CancellationToken cancellationToken); - private Placeholder currentPlaceholder; + protected abstract LeaderboardScore CreateDrawableScore(TScoreInfo model, int index); - private void replacePlaceholder(Placeholder placeholder) + protected abstract LeaderboardScore CreateDrawableTopScore(TScoreInfo model); + + private void refetchScores() { - if (placeholder != null && placeholder.Equals(currentPlaceholder)) + Debug.Assert(ThreadSafety.IsUpdateThread); + + cancelPendingWork(); + SetScores(null); + + setState(LeaderboardState.Retrieving); + + currentFetchCancellationSource = new CancellationTokenSource(); + + fetchScoresRequest = FetchScores(currentFetchCancellationSource.Token); + + if (fetchScoresRequest == null) return; - currentPlaceholder?.FadeOut(150, Easing.OutQuint).Expire(); - - if (placeholder == null) + fetchScoresRequest.Failure += e => Schedule(() => { - currentPlaceholder = null; + if (e is OperationCanceledException || currentFetchCancellationSource.IsCancellationRequested) + return; + + SetErrorState(LeaderboardState.NetworkFailure); + }); + + api?.Queue(fetchScoresRequest); + } + + private void cancelPendingWork() + { + currentFetchCancellationSource?.Cancel(); + currentScoresAsyncLoadCancellationSource?.Cancel(); + fetchScoresRequest?.Cancel(); + } + + private void updateScoresDrawables() + { + currentScoresAsyncLoadCancellationSource?.Cancel(); + + scoreFlowContainer? + .FadeOut(fade_duration, Easing.OutQuint) + .Expire(); + scoreFlowContainer = null; + + if (scores?.Any() != true) + { + setState(LeaderboardState.NoScores); return; } + LoadComponentAsync(new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Spacing = new Vector2(0f, 5f), + Padding = new MarginPadding { Top = 10, Bottom = 5 }, + ChildrenEnumerable = scores.Select((s, index) => CreateDrawableScore(s, index + 1)) + }, newFlow => + { + setState(LeaderboardState.Success); + + scrollContainer.Add(scoreFlowContainer = newFlow); + + double delay = 0; + + foreach (var s in scoreFlowContainer.Children) + { + using (s.BeginDelayedSequence(delay)) + s.Show(); + + delay += 50; + } + + scrollContainer.ScrollToStart(false); + }, (currentScoresAsyncLoadCancellationSource = new CancellationTokenSource()).Token); + } + + #region Placeholder handling + + private Placeholder placeholder; + + private void setState(LeaderboardState state) + { + if (state == this.state) + return; + + if (state == LeaderboardState.Retrieving) + loading.Show(); + else + loading.Hide(); + + this.state = state; + + placeholder?.FadeOut(150, Easing.OutQuint).Expire(); + + placeholder = getPlaceholderFor(state); + + if (placeholder == null) + return; + placeholderContainer.Child = placeholder; placeholder.ScaleTo(0.8f).Then().ScaleTo(1, fade_duration * 3, Easing.OutQuint); placeholder.FadeInFromZero(fade_duration, Easing.OutQuint); - - currentPlaceholder = placeholder; } - protected virtual bool FadeBottom => true; - protected virtual bool FadeTop => false; + private Placeholder getPlaceholderFor(LeaderboardState state) + { + switch (state) + { + case LeaderboardState.NetworkFailure: + return new ClickablePlaceholder(@"Couldn't fetch scores!", FontAwesome.Solid.Sync) + { + Action = RefetchScores + }; + + case LeaderboardState.NoneSelected: + return new MessagePlaceholder(@"Please select a beatmap!"); + + case LeaderboardState.Unavailable: + return new MessagePlaceholder(@"Leaderboards are not available for this beatmap!"); + + case LeaderboardState.NoScores: + return new MessagePlaceholder(@"No records yet!"); + + case LeaderboardState.NotLoggedIn: + return new LoginPlaceholder(@"Please sign in to view online leaderboards!"); + + case LeaderboardState.NotSupporter: + return new MessagePlaceholder(@"Please invest in an osu!supporter tag to view this leaderboard!"); + + case LeaderboardState.Retrieving: + return null; + + case LeaderboardState.Success: + return null; + + default: + throw new ArgumentOutOfRangeException(); + } + } + + #endregion + + #region Fade handling protected override void UpdateAfterChildren() { @@ -356,30 +356,29 @@ namespace osu.Game.Online.Leaderboards if (!scrollContainer.IsScrolledToEnd()) fadeBottom -= LeaderboardScore.HEIGHT; - if (scrollFlow == null) + if (scoreFlowContainer == null) return; - foreach (var c in scrollFlow.Children) + foreach (var c in scoreFlowContainer.Children) { - float topY = c.ToSpaceOfOtherDrawable(Vector2.Zero, scrollFlow).Y; + float topY = c.ToSpaceOfOtherDrawable(Vector2.Zero, scoreFlowContainer).Y; float bottomY = topY + LeaderboardScore.HEIGHT; - bool requireTopFade = FadeTop && topY <= fadeTop; - bool requireBottomFade = FadeBottom && bottomY >= fadeBottom; + bool requireBottomFade = bottomY >= fadeBottom; - if (!requireTopFade && !requireBottomFade) + if (!requireBottomFade) c.Colour = Color4.White; else if (topY > fadeBottom + LeaderboardScore.HEIGHT || bottomY < fadeTop - LeaderboardScore.HEIGHT) c.Colour = Color4.Transparent; else { - if (bottomY - fadeBottom > 0 && FadeBottom) + if (bottomY - fadeBottom > 0) { c.Colour = ColourInfo.GradientVertical( Color4.White.Opacity(Math.Min(1 - (topY - fadeBottom) / LeaderboardScore.HEIGHT, 1)), Color4.White.Opacity(Math.Min(1 - (bottomY - fadeBottom) / LeaderboardScore.HEIGHT, 1))); } - else if (FadeTop) + else { c.Colour = ColourInfo.GradientVertical( Color4.White.Opacity(Math.Min(1 - (fadeTop - topY) / LeaderboardScore.HEIGHT, 1)), @@ -389,8 +388,6 @@ namespace osu.Game.Online.Leaderboards } } - protected abstract LeaderboardScore CreateDrawableScore(TScoreInfo model, int index); - - protected abstract LeaderboardScore CreateDrawableTopScore(TScoreInfo model); + #endregion } } diff --git a/osu.Game/Online/Leaderboards/PlaceholderState.cs b/osu.Game/Online/Leaderboards/LeaderboardState.cs similarity index 87% rename from osu.Game/Online/Leaderboards/PlaceholderState.cs rename to osu.Game/Online/Leaderboards/LeaderboardState.cs index 297241fa73..75e2c6e6db 100644 --- a/osu.Game/Online/Leaderboards/PlaceholderState.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardState.cs @@ -3,9 +3,9 @@ namespace osu.Game.Online.Leaderboards { - public enum PlaceholderState + public enum LeaderboardState { - Successful, + Success, Retrieving, NetworkFailure, Unavailable, diff --git a/osu.Game/Screens/OnlinePlay/Match/Components/MatchLeaderboard.cs b/osu.Game/Screens/OnlinePlay/Match/Components/MatchLeaderboard.cs index 134e083c42..ea7de917e2 100644 --- a/osu.Game/Screens/OnlinePlay/Match/Components/MatchLeaderboard.cs +++ b/osu.Game/Screens/OnlinePlay/Match/Components/MatchLeaderboard.cs @@ -1,8 +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; -using System.Collections.Generic; +using System.Threading; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Game.Online.API; @@ -25,14 +24,14 @@ namespace osu.Game.Screens.OnlinePlay.Match.Components if (id.NewValue == null) return; - Scores = null; - UpdateScores(); + SetScores(null); + RefetchScores(); }, true); } protected override bool IsOnlineScope => true; - protected override APIRequest FetchScores(Action> scoresCallback) + protected override APIRequest FetchScores(CancellationToken cancellationToken) { if (roomId.Value == null) return null; @@ -41,8 +40,10 @@ namespace osu.Game.Screens.OnlinePlay.Match.Components req.Success += r => { - scoresCallback?.Invoke(r.Leaderboard); - TopScore = r.UserScore; + if (cancellationToken.IsCancellationRequested) + return; + + SetScores(r.Leaderboard, r.UserScore); }; return req; diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs index 4114a5e9a0..542851cb0f 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs @@ -220,7 +220,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists protected override Screen CreateGameplayScreen() => new PlayerLoader(() => new PlaylistsPlayer(Room, SelectedItem.Value) { - Exited = () => leaderboard.RefreshScores() + Exited = () => leaderboard.RefetchScores() }); } } diff --git a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs index 31cbe91f5c..907a2c9bda 100644 --- a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs +++ b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs @@ -25,12 +25,6 @@ namespace osu.Game.Screens.Select.Leaderboards { public Action ScoreSelected; - [Resolved] - private RulesetStore rulesets { get; set; } - - [Resolved] - private RealmAccess realm { get; set; } - private BeatmapInfo beatmapInfo; public BeatmapInfo BeatmapInfo @@ -39,26 +33,13 @@ namespace osu.Game.Screens.Select.Leaderboards set { if (beatmapInfo == null && value == null) - { - // always null scores to ensure a correct initial display. - // see weird `scoresLoadedOnce` logic in base implementation. - Scores = null; return; - } if (beatmapInfo?.Equals(value) == true) return; beatmapInfo = value; - Scores = null; - - if (IsOnlineScope) - UpdateScores(); - else - { - if (IsLoaded) - refreshRealmSubscription(); - } + RefetchScores(); } } @@ -77,7 +58,7 @@ namespace osu.Game.Screens.Select.Leaderboards filterMods = value; - UpdateScores(); + RefetchScores(); } } @@ -93,115 +74,58 @@ namespace osu.Game.Screens.Select.Leaderboards [Resolved] private IAPIProvider api { get; set; } - [BackgroundDependencyLoader] - private void load() - { - ruleset.ValueChanged += _ => UpdateScores(); - mods.ValueChanged += _ => - { - if (filterMods) - UpdateScores(); - }; - } + [Resolved] + private RulesetStore rulesets { get; set; } - protected override void LoadComplete() - { - base.LoadComplete(); - - refreshRealmSubscription(); - } + [Resolved] + private RealmAccess realm { get; set; } private IDisposable scoreSubscription; - private void refreshRealmSubscription() + [BackgroundDependencyLoader] + private void load() { - scoreSubscription?.Dispose(); - scoreSubscription = null; - - if (beatmapInfo == null) - return; - - scoreSubscription = realm.RegisterForNotifications(r => - r.All() - .Filter($"{nameof(ScoreInfo.BeatmapInfo)}.{nameof(BeatmapInfo.ID)} = $0", beatmapInfo.ID), - (_, changes, ___) => - { - if (!IsOnlineScope) - RefreshScores(); - }); - } - - protected override void Reset() - { - base.Reset(); - TopScore = null; + ruleset.ValueChanged += _ => RefetchScores(); + mods.ValueChanged += _ => + { + if (filterMods) + RefetchScores(); + }; } protected override bool IsOnlineScope => Scope != BeatmapLeaderboardScope.Local; - private CancellationTokenSource loadCancellationSource; - - protected override APIRequest FetchScores(Action> scoresCallback) + protected override APIRequest FetchScores(CancellationToken cancellationToken) { - loadCancellationSource?.Cancel(); - loadCancellationSource = new CancellationTokenSource(); - - var cancellationToken = loadCancellationSource.Token; - var fetchBeatmapInfo = BeatmapInfo; if (fetchBeatmapInfo == null) { - PlaceholderState = PlaceholderState.NoneSelected; + SetErrorState(LeaderboardState.NoneSelected); return null; } if (Scope == BeatmapLeaderboardScope.Local) { - realm.Run(r => - { - var scores = r.All() - .AsEnumerable() - // TODO: update to use a realm filter directly (or at least figure out the beatmap part to reduce scope). - .Where(s => !s.DeletePending && s.BeatmapInfo.ID == fetchBeatmapInfo.ID && s.Ruleset.ShortName == ruleset.Value.ShortName); - - 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 => scoresCallback?.Invoke(ordered.GetResultSafely()), TaskContinuationOptions.OnlyOnRanToCompletion); - }); - + subscribeToLocalScores(cancellationToken); return null; } if (api?.IsLoggedIn != true) { - PlaceholderState = PlaceholderState.NotLoggedIn; + SetErrorState(LeaderboardState.NotLoggedIn); return null; } if (fetchBeatmapInfo.OnlineID <= 0 || fetchBeatmapInfo.Status <= BeatmapOnlineStatus.Pending) { - PlaceholderState = PlaceholderState.Unavailable; + SetErrorState(LeaderboardState.Unavailable); return null; } if (!api.LocalUser.Value.IsSupporter && (Scope != BeatmapLeaderboardScope.Global || filterMods)) { - PlaceholderState = PlaceholderState.NotSupporter; + SetErrorState(LeaderboardState.NotSupporter); return null; } @@ -223,8 +147,7 @@ namespace osu.Game.Screens.Select.Leaderboards if (cancellationToken.IsCancellationRequested) return; - scoresCallback?.Invoke(task.GetResultSafely()); - TopScore = r.UserScore?.CreateScoreInfo(rulesets, fetchBeatmapInfo); + SetScores(task.GetResultSafely(), r.UserScore?.CreateScoreInfo(rulesets, fetchBeatmapInfo)); }), TaskContinuationOptions.OnlyOnRanToCompletion); }; @@ -241,10 +164,56 @@ namespace osu.Game.Screens.Select.Leaderboards Action = () => ScoreSelected?.Invoke(model) }; + private void subscribeToLocalScores(CancellationToken cancellationToken) + { + scoreSubscription?.Dispose(); + scoreSubscription = null; + + if (beatmapInfo == null) + return; + + scoreSubscription = realm.RegisterForNotifications(r => + r.All().Filter($"{nameof(ScoreInfo.BeatmapInfo)}.{nameof(BeatmapInfo.ID)} == $0" + + $" AND {nameof(ScoreInfo.Ruleset)}.{nameof(RulesetInfo.ShortName)} == $1" + + $" AND {nameof(ScoreInfo.DeletePending)} == false" + , beatmapInfo.ID, ruleset.Value.ShortName), localScoresChanged); + + void localScoresChanged(IRealmCollection sender, ChangeSet changes, Exception exception) + { + if (cancellationToken.IsCancellationRequested) + return; + + var scores = sender.AsEnumerable(); + + 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 => Schedule(() => + { + if (cancellationToken.IsCancellationRequested) + return; + + SetScores(ordered.GetResultSafely()); + }), TaskContinuationOptions.OnlyOnRanToCompletion); + } + } + protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); - scoreSubscription?.Dispose(); } } diff --git a/osu.Game/Screens/Select/PlayBeatmapDetailArea.cs b/osu.Game/Screens/Select/PlayBeatmapDetailArea.cs index b8b8e3e4bc..09f75b7658 100644 --- a/osu.Game/Screens/Select/PlayBeatmapDetailArea.cs +++ b/osu.Game/Screens/Select/PlayBeatmapDetailArea.cs @@ -53,7 +53,7 @@ namespace osu.Game.Screens.Select { base.Refresh(); - Leaderboard.RefreshScores(); + Leaderboard.RefetchScores(); } protected override void OnTabChanged(BeatmapDetailAreaTabItem tab, bool selectedMods)