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

371 lines
13 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.Linq;
using System.Threading;
2018-04-13 17:19:50 +08:00
using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Threading;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Cursor;
2018-04-13 17:19:50 +08:00
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API;
using osu.Game.Online.Placeholders;
using osuTK;
using osuTK.Graphics;
2018-04-13 17:19:50 +08:00
namespace osu.Game.Online.Leaderboards
2018-04-13 17:19:50 +08:00
{
public abstract class Leaderboard<TScope, TScoreInfo> : Container, IOnlineComponent
2018-04-13 17:19:50 +08:00
{
private const double fade_duration = 300;
private readonly OsuScrollContainer scrollContainer;
2018-04-13 17:19:50 +08:00
private readonly Container placeholderContainer;
2018-12-22 14:20:35 +08:00
private FillFlowContainer<LeaderboardScore> scrollFlow;
2018-04-13 17:19:50 +08:00
private readonly LoadingAnimation loading;
private ScheduledDelegate showScoresDelegate;
private CancellationTokenSource showScoresCancellationSource;
2018-04-13 17:19:50 +08:00
private bool scoresLoadedOnce;
2019-09-19 13:52:31 +08:00
private readonly Container content;
protected override Container<Drawable> Content => content;
private IEnumerable<TScoreInfo> scores;
public IEnumerable<TScoreInfo> Scores
2018-04-13 17:19:50 +08:00
{
get => scores;
2018-04-13 17:19:50 +08:00
set
{
scores = value;
scoresLoadedOnce = true;
2018-04-13 17:19:50 +08:00
scrollFlow?.FadeOut(fade_duration, Easing.OutQuint).Expire();
scrollFlow = null;
loading.Hide();
2019-01-07 18:31:05 +08:00
showScoresDelegate?.Cancel();
showScoresCancellationSource?.Cancel();
2018-04-13 17:19:50 +08:00
if (scores == null || !scores.Any())
return;
// ensure placeholder is hidden when displaying scores
PlaceholderState = PlaceholderState.Successful;
2019-09-19 13:52:31 +08:00
var scoreFlow = CreateScoreFlow();
scoreFlow.ChildrenEnumerable = scores.Select((s, index) => CreateDrawableScore(s, index + 1));
// schedule because we may not be loaded yet (LoadComponentAsync complains).
2019-09-19 13:52:31 +08:00
showScoresDelegate = Schedule(() => LoadComponentAsync(scoreFlow, _ =>
2018-04-13 17:19:50 +08:00
{
2019-09-19 13:52:31 +08:00
scrollContainer.Add(scrollFlow = scoreFlow);
2018-04-13 17:19:50 +08:00
int i = 0;
2019-04-01 11:16:05 +08:00
2018-12-27 14:30:02 +08:00
foreach (var s in scrollFlow.Children)
2019-11-11 19:53:22 +08:00
{
using (s.BeginDelayedSequence(i++ * 50, true))
s.Show();
2019-11-11 19:53:22 +08:00
}
2018-04-13 17:19:50 +08:00
scrollContainer.ScrollTo(0f, false);
}, (showScoresCancellationSource = new CancellationTokenSource()).Token));
2018-04-13 17:19:50 +08:00
}
}
2018-12-27 14:30:02 +08:00
protected virtual FillFlowContainer<LeaderboardScore> CreateScoreFlow()
=> new FillFlowContainer<LeaderboardScore>
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Spacing = new Vector2(0f, 5f),
Padding = new MarginPadding { Top = 10, Bottom = 5 },
};
private TScope scope;
2018-04-13 17:19:50 +08:00
public TScope Scope
2018-04-13 17:19:50 +08:00
{
get => scope;
2018-04-13 17:19:50 +08:00
set
{
2019-11-13 22:35:50 +08:00
if (EqualityComparer<TScope>.Default.Equals(value, scope))
2018-04-13 17:19:50 +08:00
return;
scope = value;
UpdateScores();
2018-04-13 17:19:50 +08:00
}
}
private PlaceholderState placeholderState;
/// <summary>
/// Update the placeholder visibility.
/// Setting this to anything other than PlaceholderState.Successful will cancel all existing retrieval requests and hide scores.
/// </summary>
2018-04-13 17:19:50 +08:00
protected PlaceholderState PlaceholderState
{
get => placeholderState;
2018-04-13 17:19:50 +08:00
set
{
if (value != PlaceholderState.Successful)
{
2019-09-19 14:23:33 +08:00
Reset();
2018-04-13 17:19:50 +08:00
}
if (value == placeholderState)
return;
switch (placeholderState = value)
{
case PlaceholderState.NetworkFailure:
replacePlaceholder(new RetrievalFailurePlaceholder
{
OnRetry = UpdateScores,
2018-04-13 17:19:50 +08:00
});
break;
2019-04-01 11:16:05 +08:00
case PlaceholderState.NoneSelected:
replacePlaceholder(new MessagePlaceholder(@"Please select a beatmap!"));
break;
2018-04-13 17:19:50 +08:00
case PlaceholderState.Unavailable:
replacePlaceholder(new MessagePlaceholder(@"Leaderboards are not available for this beatmap!"));
break;
2019-04-01 11:16:05 +08:00
2018-04-13 17:19:50 +08:00
case PlaceholderState.NoScores:
replacePlaceholder(new MessagePlaceholder(@"No records yet!"));
break;
2019-04-01 11:16:05 +08:00
2018-04-13 17:19:50 +08:00
case PlaceholderState.NotLoggedIn:
2020-01-08 16:22:51 +08:00
replacePlaceholder(new LoginPlaceholder());
2018-04-13 17:19:50 +08:00
break;
2019-04-01 11:16:05 +08:00
2018-04-13 17:19:50 +08:00
case PlaceholderState.NotSupporter:
2018-06-28 08:57:55 +08:00
replacePlaceholder(new MessagePlaceholder(@"Please invest in an osu!supporter tag to view this leaderboard!"));
2018-04-13 17:19:50 +08:00
break;
2019-04-01 11:16:05 +08:00
2018-04-13 17:19:50 +08:00
default:
replacePlaceholder(null);
break;
}
}
}
protected Leaderboard()
2018-04-13 17:19:50 +08:00
{
2019-09-19 14:23:33 +08:00
InternalChildren = new Drawable[]
2018-04-13 17:19:50 +08:00
{
2019-09-19 13:52:31 +08:00
new GridContainer
2018-04-13 17:19:50 +08:00
{
RelativeSizeAxes = Axes.Both,
2019-09-19 13:52:31 +08:00
RowDimensions = new[]
{
new Dimension(),
new Dimension(GridSizeMode.AutoSize),
},
Content = new[]
{
new Drawable[]
{
new OsuContextMenuContainer
2019-09-19 13:52:31 +08:00
{
RelativeSizeAxes = Axes.Both,
Child = scrollContainer = new OsuScrollContainer
{
RelativeSizeAxes = Axes.Both,
ScrollbarVisible = false,
}
2019-09-19 13:52:31 +08:00
}
},
new Drawable[]
{
content = new Container
{
AutoSizeAxes = Axes.Y,
RelativeSizeAxes = Axes.X,
},
}
},
2018-04-13 17:19:50 +08:00
},
loading = new LoadingAnimation(),
placeholderContainer = new Container
{
RelativeSizeAxes = Axes.Both
},
};
}
2019-09-19 14:23:33 +08:00
protected virtual void Reset()
{
getScoresRequest?.Cancel();
getScoresRequest = null;
Scores = null;
}
private IAPIProvider api;
private ScheduledDelegate pendingUpdateScores;
2018-04-13 17:19:50 +08:00
[BackgroundDependencyLoader(true)]
private void load(IAPIProvider api)
2018-04-13 17:19:50 +08:00
{
this.api = api;
2018-12-27 14:30:02 +08:00
api?.Register(this);
2018-04-13 17:19:50 +08:00
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
2018-12-27 14:30:02 +08:00
api?.Unregister(this);
2018-04-13 17:19:50 +08:00
}
public void RefreshScores() => UpdateScores();
private APIRequest getScoresRequest;
2018-04-13 17:19:50 +08:00
2019-07-21 09:42:40 +08:00
protected abstract bool IsOnlineScope { get; }
2019-07-21 08:07:27 +08:00
public void APIStateChanged(IAPIProvider api, APIState state)
2018-04-13 17:19:50 +08:00
{
switch (state)
{
case APIState.Online:
case APIState.Offline:
2019-07-21 09:42:40 +08:00
if (IsOnlineScope)
2019-07-21 08:07:27 +08:00
UpdateScores();
break;
}
2018-04-13 17:19:50 +08:00
}
protected void UpdateScores()
2018-04-13 17:19:50 +08:00
{
// 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;
getScoresRequest?.Cancel();
getScoresRequest = null;
2018-04-13 17:19:50 +08:00
pendingUpdateScores?.Cancel();
pendingUpdateScores = Schedule(() =>
2018-04-13 17:19:50 +08:00
{
PlaceholderState = PlaceholderState.Retrieving;
loading.Show();
2018-04-13 17:19:50 +08:00
getScoresRequest = FetchScores(scores => Schedule(() =>
{
Scores = scores;
PlaceholderState = Scores.Any() ? PlaceholderState.Successful : PlaceholderState.NoScores;
}));
if (getScoresRequest == null)
return;
2018-04-13 17:19:50 +08:00
getScoresRequest.Failure += e => Schedule(() =>
{
if (e is OperationCanceledException)
return;
PlaceholderState = PlaceholderState.NetworkFailure;
});
api.Queue(getScoresRequest);
});
2018-04-13 17:19:50 +08:00
}
2019-03-05 17:48:59 +08:00
/// <summary>
/// Performs a fetch/refresh of scores to be displayed.
/// </summary>
/// <param name="scoresCallback">A callback which should be called when fetching is completed. Scheduling is not required.</param>
/// <returns>An <see cref="APIRequest"/> responsible for the fetch operation. This will be queued and performed automatically.</returns>
protected abstract APIRequest FetchScores(Action<IEnumerable<TScoreInfo>> scoresCallback);
private Placeholder currentPlaceholder;
2018-04-13 17:19:50 +08:00
private void replacePlaceholder(Placeholder placeholder)
{
if (placeholder != null && placeholder.Equals(currentPlaceholder))
2018-04-13 17:19:50 +08:00
return;
currentPlaceholder?.FadeOut(150, Easing.OutQuint).Expire();
2018-04-13 17:19:50 +08:00
if (placeholder == null)
{
currentPlaceholder = null;
2018-04-13 17:19:50 +08:00
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);
currentPlaceholder = placeholder;
2018-04-13 17:19:50 +08:00
}
2018-12-27 14:30:02 +08:00
protected virtual bool FadeBottom => true;
protected virtual bool FadeTop => false;
2018-12-27 14:30:02 +08:00
protected override void UpdateAfterChildren()
2018-04-13 17:19:50 +08:00
{
base.UpdateAfterChildren();
2018-04-13 17:19:50 +08:00
2018-12-27 14:30:02 +08:00
var fadeBottom = scrollContainer.Current + scrollContainer.DrawHeight;
var fadeTop = scrollContainer.Current + LeaderboardScore.HEIGHT;
2018-04-13 17:19:50 +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 (scrollFlow == null)
return;
foreach (var c in scrollFlow.Children)
{
var topY = c.ToSpaceOfOtherDrawable(Vector2.Zero, scrollFlow).Y;
var bottomY = topY + LeaderboardScore.HEIGHT;
2018-12-27 14:30:02 +08:00
bool requireTopFade = FadeTop && topY <= fadeTop;
bool requireBottomFade = FadeBottom && bottomY >= fadeBottom;
if (!requireTopFade && !requireBottomFade)
2018-04-13 17:19:50 +08:00
c.Colour = Color4.White;
2018-12-27 14:30:02 +08:00
else if (topY > fadeBottom + LeaderboardScore.HEIGHT || bottomY < fadeTop - LeaderboardScore.HEIGHT)
2018-04-13 17:19:50 +08:00
c.Colour = Color4.Transparent;
else
{
2018-12-27 14:30:02 +08:00
if (bottomY - fadeBottom > 0 && FadeBottom)
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
}
2018-12-27 14:30:02 +08:00
else if (FadeTop)
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
}
2018-04-13 17:19:50 +08:00
}
}
}
protected abstract LeaderboardScore CreateDrawableScore(TScoreInfo model, int index);
2018-04-13 17:19:50 +08:00
}
}