mirror of
https://github.com/ppy/osu
synced 2024-12-14 10:57:41 +00:00
Merge pull request #16703 from peppy/leaderboard-component-fixes
Rewrite `Leaderboard` component to bring up to current code standards
This commit is contained in:
commit
e9f3e7f5cb
@ -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> 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<ScoreInfo> scores, ScoreInfo userScore = default) => base.SetScores(scores, userScore);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -128,21 +128,16 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
scoreManager.Undelete(r.All<ScoreInfo>().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<LeaderboardScore>().Any());
|
||||
|
||||
// Ensure the leaderboard items have finished showing up
|
||||
AddStep("finish transforms", () => leaderboard.FinishTransforms(true));
|
||||
AddUntilStep("wait for drawables", () => leaderboard.ChildrenOfType<LeaderboardScore>().Any());
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
@ -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<TScope, TScoreInfo> : Container
|
||||
/// <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>
|
||||
public abstract class Leaderboard<TScope, TScoreInfo> : CompositeDrawable
|
||||
{
|
||||
/// <summary>
|
||||
/// The currently displayed scores.
|
||||
/// </summary>
|
||||
public IEnumerable<TScoreInfo> Scores => scores;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the current scope should refetch in response to changes in API connectivity state.
|
||||
/// </summary>
|
||||
protected abstract bool IsOnlineScope { get; }
|
||||
|
||||
private const double fade_duration = 300;
|
||||
|
||||
private readonly OsuScrollContainer scrollContainer;
|
||||
private readonly Container placeholderContainer;
|
||||
private readonly UserTopScoreContainer<TScoreInfo> topScoreContainer;
|
||||
private readonly UserTopScoreContainer<TScoreInfo> userScoreContainer;
|
||||
|
||||
private FillFlowContainer<LeaderboardScore> scrollFlow;
|
||||
private FillFlowContainer<LeaderboardScore> 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<Drawable> Content => content;
|
||||
[Resolved(CanBeNull = true)]
|
||||
private IAPIProvider api { get; set; }
|
||||
|
||||
private readonly IBindable<APIState> apiState = new Bindable<APIState>();
|
||||
|
||||
private ICollection<TScoreInfo> scores;
|
||||
|
||||
public ICollection<TScoreInfo> 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<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;
|
||||
|
||||
public TScope Scope
|
||||
@ -126,62 +78,7 @@ namespace osu.Game.Online.Leaderboards
|
||||
return;
|
||||
|
||||
scope = value;
|
||||
RefreshScores();
|
||||
}
|
||||
}
|
||||
|
||||
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>
|
||||
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<TScoreInfo>(CreateDrawableTopScore)
|
||||
},
|
||||
userScoreContainer = new UserTopScoreContainer<TScoreInfo>(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> apiState = new Bindable<APIState>();
|
||||
|
||||
[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;
|
||||
/// <summary>
|
||||
/// Perform a full refetch of scores using current criteria.
|
||||
/// </summary>
|
||||
public void RefetchScores() => Scheduler.AddOnce(refetchScores);
|
||||
|
||||
protected abstract bool IsOnlineScope { get; }
|
||||
|
||||
private void onlineStateChanged(ValueChangedEvent<APIState> state) => Schedule(() =>
|
||||
/// <summary>
|
||||
/// Call when a retrieval or display failure happened to show a relevant message to the user.
|
||||
/// </summary>
|
||||
/// <param name="state">The state to display.</param>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <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)
|
||||
{
|
||||
// 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);
|
||||
}
|
||||
|
||||
/// <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>
|
||||
/// <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(Action<IEnumerable<TScoreInfo>> 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<LeaderboardScore>
|
||||
{
|
||||
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
|
||||
}
|
||||
}
|
||||
|
@ -3,9 +3,9 @@
|
||||
|
||||
namespace osu.Game.Online.Leaderboards
|
||||
{
|
||||
public enum PlaceholderState
|
||||
public enum LeaderboardState
|
||||
{
|
||||
Successful,
|
||||
Success,
|
||||
Retrieving,
|
||||
NetworkFailure,
|
||||
Unavailable,
|
@ -1,8 +1,7 @@
|
||||
// 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.
|
||||
|
||||
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<IEnumerable<APIUserScoreAggregate>> 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;
|
||||
|
@ -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()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -25,12 +25,6 @@ namespace osu.Game.Screens.Select.Leaderboards
|
||||
{
|
||||
public Action<ScoreInfo> 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<ScoreInfo>()
|
||||
.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<IEnumerable<ScoreInfo>> 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<ScoreInfo>()
|
||||
.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<ScoreInfo>().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<ScoreInfo> 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();
|
||||
}
|
||||
}
|
||||
|
@ -53,7 +53,7 @@ namespace osu.Game.Screens.Select
|
||||
{
|
||||
base.Refresh();
|
||||
|
||||
Leaderboard.RefreshScores();
|
||||
Leaderboard.RefetchScores();
|
||||
}
|
||||
|
||||
protected override void OnTabChanged(BeatmapDetailAreaTabItem tab, bool selectedMods)
|
||||
|
Loading…
Reference in New Issue
Block a user