1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-15 15:12:57 +08:00
osu-lazer/osu.Game/Online/Leaderboards/Leaderboard.cs

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

419 lines
14 KiB
C#
Raw Normal View History

// 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.
2018-04-13 17:19:50 +08:00
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Development;
2017-03-16 00:57:41 +08:00
using osu.Framework.Extensions.Color4Extensions;
2017-03-04 15:37:34 +08:00
using osu.Framework.Graphics;
2017-03-16 00:57:41 +08:00
using osu.Framework.Graphics.Colour;
2017-03-04 15:37:34 +08:00
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Cursor;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API;
using osu.Game.Online.Placeholders;
using osuTK;
using osuTK.Graphics;
using osu.Game.Localisation;
2018-04-13 17:19:50 +08:00
namespace osu.Game.Online.Leaderboards
2017-03-04 15:37:34 +08:00
{
2022-01-28 20:16:29 +08:00
/// <summary>
/// A leaderboard which displays a scrolling list of top scores, along with a single "user best"
/// for the local user.
/// </summary>
/// <typeparam name="TScope">The scope of the leaderboard (ie. global or local).</typeparam>
/// <typeparam name="TScoreInfo">The score model class.</typeparam>
2022-01-28 20:38:23 +08:00
public abstract partial class Leaderboard<TScope, TScoreInfo> : CompositeDrawable
2017-03-04 15:37:34 +08:00
{
/// <summary>
/// The currently displayed scores.
/// </summary>
public IBindableList<TScoreInfo> Scores => scores;
private readonly BindableList<TScoreInfo> scores = new BindableList<TScoreInfo>();
2022-01-28 22:17:06 +08:00
/// <summary>
/// Whether the current scope should refetch in response to changes in API connectivity state.
/// </summary>
2022-01-28 21:28:13 +08:00
protected abstract bool IsOnlineScope { get; }
private const double fade_duration = 300;
2018-04-13 17:19:50 +08:00
private readonly OsuScrollContainer scrollContainer;
2017-11-26 17:33:49 +08:00
private readonly Container placeholderContainer;
private readonly UserTopScoreContainer<TScoreInfo> userScoreContainer;
2018-04-13 17:19:50 +08:00
private FillFlowContainer<LeaderboardScore>? scoreFlowContainer;
2018-04-13 17:19:50 +08:00
private readonly LoadingSpinner loading;
2018-04-13 17:19:50 +08:00
private CancellationTokenSource? currentFetchCancellationSource;
private CancellationTokenSource? currentScoresAsyncLoadCancellationSource;
2018-04-13 17:19:50 +08:00
private APIRequest? fetchScoresRequest;
2022-01-28 20:33:22 +08:00
2022-01-31 00:12:03 +08:00
private LeaderboardState state;
2022-01-28 20:33:22 +08:00
[Resolved(CanBeNull = true)]
private IAPIProvider? api { get; set; }
2022-01-28 20:33:22 +08:00
private readonly IBindable<APIState> apiState = new Bindable<APIState>();
private TScope scope = default!;
2018-04-13 17:19:50 +08:00
public TScope Scope
{
get => scope;
set
{
2019-11-13 22:35:50 +08:00
if (EqualityComparer<TScope>.Default.Equals(value, scope))
return;
2018-04-13 17:19:50 +08:00
scope = value;
RefetchScores();
}
}
2018-04-13 17:19:50 +08:00
protected Leaderboard()
2017-03-04 15:37:34 +08:00
{
2019-09-19 14:23:33 +08:00
InternalChildren = new Drawable[]
2017-03-04 15:37:34 +08:00
{
new OsuContextMenuContainer
2017-03-04 15:37:34 +08:00
{
RelativeSizeAxes = Axes.Both,
Masking = true,
Child = new GridContainer
2019-09-19 13:52:31 +08:00
{
RelativeSizeAxes = Axes.Both,
RowDimensions = new[]
2019-09-19 13:52:31 +08:00
{
new Dimension(),
new Dimension(GridSizeMode.AutoSize),
},
Content = new[]
{
new Drawable[]
2019-09-19 13:52:31 +08:00
{
scrollContainer = new OsuScrollContainer
{
RelativeSizeAxes = Axes.Both,
ScrollbarVisible = false,
}
},
new Drawable[]
2019-09-19 13:52:31 +08:00
{
2022-01-31 12:45:49 +08:00
userScoreContainer = new UserTopScoreContainer<TScoreInfo>(CreateDrawableTopScore)
2019-09-19 13:52:31 +08:00
},
},
},
2017-03-04 15:37:34 +08:00
},
loading = new LoadingSpinner(),
placeholderContainer = new Container
{
RelativeSizeAxes = Axes.Both
},
2017-03-04 15:37:34 +08:00
};
}
2018-04-13 17:19:50 +08:00
2022-01-28 20:33:22 +08:00
protected override void LoadComplete()
{
2022-01-28 20:33:22 +08:00
base.LoadComplete();
2019-07-21 08:07:27 +08:00
2022-01-28 20:33:22 +08:00
if (api != null)
{
2022-01-28 20:33:22 +08:00
apiState.BindTo(api.State);
apiState.BindValueChanged(state =>
{
switch (state.NewValue)
{
case APIState.Online:
case APIState.Offline:
if (IsOnlineScope)
RefetchScores();
2019-07-21 08:07:27 +08:00
2022-01-28 20:33:22 +08:00
break;
}
});
}
2022-01-28 20:33:22 +08:00
RefetchScores();
}
2018-04-13 17:19:50 +08:00
/// <summary>
/// Perform a full refetch of scores using current criteria.
/// </summary>
public void RefetchScores() => Scheduler.AddOnce(refetchScores);
2021-06-14 13:26:40 +08:00
/// <summary>
/// Clear all scores from the display.
/// </summary>
public void ClearScores()
{
cancelPendingWork();
SetScores(null);
}
/// <summary>
/// Call when a retrieval or display failure happened to show a relevant message to the user.
/// </summary>
2022-01-31 00:12:03 +08:00
/// <param name="state">The state to display.</param>
protected void SetErrorState(LeaderboardState state)
{
2022-01-31 00:12:03 +08:00
switch (state)
{
2022-01-31 00:12:03 +08:00
case LeaderboardState.NoScores:
case LeaderboardState.Retrieving:
case LeaderboardState.Success:
throw new InvalidOperationException($"State {state} cannot be set by a leaderboard implementation.");
}
2022-09-22 19:35:26 +08:00
Debug.Assert(!scores.Any());
2022-01-31 00:12:03 +08:00
setState(state);
}
/// <summary>
/// Call when retrieved scores are ready to be displayed.
/// </summary>
/// <param name="scores">The scores to display.</param>
/// <param name="userScore">The user top score, if any.</param>
protected void SetScores(IEnumerable<TScoreInfo>? scores, TScoreInfo? userScore = default)
{
this.scores.Clear();
if (scores != null)
this.scores.AddRange(scores);
// Non-delayed schedule may potentially run inline (due to IsMainThread check passing) after leaderboard is disposed.
// This is guarded against in BeatmapLeaderboard via web request cancellation, but let's be extra safe.
if (!IsDisposed)
{
// Schedule needs to be non-delayed here for the weird logic in refetchScores to work.
// If it is removed, the placeholder will be incorrectly updated to "no scores" rather than "retrieving".
// This whole flow should be refactored in the future.
Scheduler.Add(applyNewScores, false);
}
void applyNewScores()
{
userScoreContainer.Score.Value = userScore;
if (userScore == null)
userScoreContainer.Hide();
else
userScoreContainer.Show();
updateScoresDrawables();
}
}
/// <summary>
/// Performs a fetch/refresh of scores to be displayed.
/// </summary>
2022-01-28 22:14:26 +08:00
/// <param name="cancellationToken"></param>
/// <returns>An <see cref="APIRequest"/> responsible for the fetch operation. This will be queued and performed automatically.</returns>
protected abstract APIRequest? FetchScores(CancellationToken cancellationToken);
protected abstract LeaderboardScore CreateDrawableScore(TScoreInfo model, int index);
protected abstract LeaderboardScore CreateDrawableTopScore(TScoreInfo model);
private void refetchScores()
{
Debug.Assert(ThreadSafety.IsUpdateThread);
ClearScores();
2022-01-31 00:12:03 +08:00
setState(LeaderboardState.Retrieving);
2022-01-28 22:14:26 +08:00
currentFetchCancellationSource = new CancellationTokenSource();
fetchScoresRequest = FetchScores(currentFetchCancellationSource.Token);
2018-04-13 17:19:50 +08:00
2022-01-28 22:14:26 +08:00
if (fetchScoresRequest == null)
return;
2022-01-28 22:14:26 +08:00
fetchScoresRequest.Failure += e => Schedule(() =>
{
2022-01-28 22:14:26 +08:00
if (e is OperationCanceledException || currentFetchCancellationSource.IsCancellationRequested)
return;
2018-04-13 17:19:50 +08:00
2022-01-31 00:12:03 +08:00
SetErrorState(LeaderboardState.NetworkFailure);
});
2022-01-28 22:14:26 +08:00
api?.Queue(fetchScoresRequest);
}
2018-04-13 17:19:50 +08:00
2022-01-28 20:33:22 +08:00
private void cancelPendingWork()
{
2022-01-28 22:14:26 +08:00
currentFetchCancellationSource?.Cancel();
currentScoresAsyncLoadCancellationSource?.Cancel();
2022-01-28 22:14:26 +08:00
fetchScoresRequest?.Cancel();
2022-01-28 20:33:22 +08:00
}
private void updateScoresDrawables()
2022-01-28 21:28:13 +08:00
{
currentScoresAsyncLoadCancellationSource?.Cancel();
scoreFlowContainer?
.FadeOut(fade_duration, Easing.OutQuint)
.Expire();
scoreFlowContainer = null;
2022-09-22 19:35:26 +08:00
if (!scores.Any())
2022-01-28 21:28:13 +08:00
{
2022-01-31 00:12:03 +08:00
setState(LeaderboardState.NoScores);
2022-01-28 21:28:13 +08:00
return;
}
LoadComponentAsync(new FillFlowContainer<LeaderboardScore>
2022-01-28 21:28:13 +08:00
{
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 =>
2022-01-28 21:28:13 +08:00
{
2022-01-31 00:12:03 +08:00
setState(LeaderboardState.Success);
scrollContainer.Add(scoreFlowContainer = newFlow);
2022-01-28 21:28:13 +08:00
double delay = 0;
2022-01-28 21:28:13 +08:00
foreach (var s in scoreFlowContainer)
2022-01-28 21:28:13 +08:00
{
using (s.BeginDelayedSequence(delay))
2022-01-28 21:28:13 +08:00
s.Show();
delay += 50;
2022-01-28 21:28:13 +08:00
}
scrollContainer.ScrollToStart(false);
}, (currentScoresAsyncLoadCancellationSource = new CancellationTokenSource()).Token);
}
2022-01-28 21:28:13 +08:00
#region Placeholder handling
private Placeholder? placeholder;
2022-01-31 00:12:03 +08:00
private void setState(LeaderboardState state)
{
2022-01-31 00:12:03 +08:00
if (state == this.state)
return;
2018-04-13 17:19:50 +08:00
2022-01-31 00:12:03 +08:00
if (state == LeaderboardState.Retrieving)
loading.Show();
else
loading.Hide();
this.state = state;
placeholder?.FadeOut(150, Easing.OutQuint).Expire();
2022-01-31 00:12:03 +08:00
placeholder = getPlaceholderFor(state);
2018-04-13 17:19:50 +08:00
if (placeholder == null)
return;
2018-04-13 17:19:50 +08:00
placeholderContainer.Child = placeholder;
2018-04-13 17:19:50 +08:00
placeholder.ScaleTo(0.8f).Then().ScaleTo(1, fade_duration * 3, Easing.OutQuint);
placeholder.FadeInFromZero(fade_duration, Easing.OutQuint);
}
private Placeholder? getPlaceholderFor(LeaderboardState state)
{
2022-01-31 00:12:03 +08:00
switch (state)
{
2022-01-31 00:12:03 +08:00
case LeaderboardState.NetworkFailure:
return new ClickablePlaceholder(LeaderboardStrings.CouldntFetchScores, FontAwesome.Solid.Sync)
{
Action = RefetchScores
};
2022-01-31 00:12:03 +08:00
case LeaderboardState.NoneSelected:
return new MessagePlaceholder(LeaderboardStrings.PleaseSelectABeatmap);
case LeaderboardState.RulesetUnavailable:
return new MessagePlaceholder(LeaderboardStrings.LeaderboardsAreNotAvailableForThisRuleset);
case LeaderboardState.BeatmapUnavailable:
return new MessagePlaceholder(LeaderboardStrings.LeaderboardsAreNotAvailableForThisBeatmap);
2022-01-31 00:12:03 +08:00
case LeaderboardState.NoScores:
return new MessagePlaceholder(LeaderboardStrings.NoRecordsYet);
2022-01-31 00:12:03 +08:00
case LeaderboardState.NotLoggedIn:
return new LoginPlaceholder(LeaderboardStrings.PleaseSignInToViewOnlineLeaderboards);
2022-01-31 00:12:03 +08:00
case LeaderboardState.NotSupporter:
return new MessagePlaceholder(LeaderboardStrings.PleaseInvestInAnOsuSupporterTagToViewThisLeaderboard);
2022-01-31 00:12:03 +08:00
case LeaderboardState.Retrieving:
return null;
case LeaderboardState.Success:
return null;
default:
throw new ArgumentOutOfRangeException();
}
}
2018-04-13 17:19:50 +08:00
#endregion
#region Fade handling
protected override void UpdateAfterChildren()
2017-03-16 12:15:06 +08:00
{
base.UpdateAfterChildren();
2018-04-13 17:19:50 +08:00
float fadeBottom = scrollContainer.Current + scrollContainer.DrawHeight;
float fadeTop = scrollContainer.Current + LeaderboardScore.HEIGHT;
2018-04-13 17:19:50 +08:00
2017-03-19 20:49:29 +08:00
if (!scrollContainer.IsScrolledToEnd())
2018-12-27 14:30:02 +08:00
fadeBottom -= LeaderboardScore.HEIGHT;
2018-04-13 17:19:50 +08:00
if (scoreFlowContainer == null)
return;
2018-04-13 17:19:50 +08:00
foreach (var c in scoreFlowContainer)
2017-03-16 12:15:06 +08:00
{
float topY = c.ToSpaceOfOtherDrawable(Vector2.Zero, scoreFlowContainer).Y;
float bottomY = topY + LeaderboardScore.HEIGHT;
2018-04-13 17:19:50 +08:00
2022-01-28 20:49:52 +08:00
bool requireBottomFade = bottomY >= fadeBottom;
2018-12-27 14:30:02 +08:00
2022-01-28 20:49:52 +08:00
if (!requireBottomFade)
2017-03-19 20:49:29 +08:00
c.Colour = Color4.White;
2018-12-27 14:30:02 +08:00
else if (topY > fadeBottom + LeaderboardScore.HEIGHT || bottomY < fadeTop - LeaderboardScore.HEIGHT)
2017-03-19 20:49:29 +08:00
c.Colour = Color4.Transparent;
else
{
2022-01-28 20:49:52 +08:00
if (bottomY - fadeBottom > 0)
2019-11-11 19:53:22 +08:00
{
2018-12-27 14:30:02 +08:00
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)));
2019-11-11 19:53:22 +08:00
}
2022-01-28 20:49:52 +08:00
else
2019-11-11 19:53:22 +08:00
{
2018-12-27 14:30:02 +08:00
c.Colour = ColourInfo.GradientVertical(
Color4.White.Opacity(Math.Min(1 - (fadeTop - topY) / LeaderboardScore.HEIGHT, 1)),
Color4.White.Opacity(Math.Min(1 - (fadeTop - bottomY) / LeaderboardScore.HEIGHT, 1)));
2019-11-11 19:53:22 +08:00
}
}
2017-03-16 12:15:06 +08:00
}
}
#endregion
2017-03-04 15:37:34 +08:00
}
}