1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-14 21:02:55 +08:00

Merge pull request #9706 from smoogipoo/multi-pagination

This commit is contained in:
Dean Herbert 2020-07-31 23:08:34 +09:00 committed by GitHub
commit 60ef727304
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 751 additions and 107 deletions

View File

@ -3,14 +3,24 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using JetBrains.Annotations;
using Newtonsoft.Json.Linq;
using NUnit.Framework;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.Multiplayer;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Screens.Multi.Ranking;
using osu.Game.Screens.Ranking;
using osu.Game.Tests.Beatmaps;
using osu.Game.Users;
@ -18,43 +28,134 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
public class TestSceneTimeshiftResultsScreen : ScreenTestScene
{
private bool roomsReceived;
private const int scores_per_result = 10;
private TestResultsScreen resultsScreen;
private int currentScoreId;
private bool requestComplete;
[SetUp]
public void Setup() => Schedule(() =>
{
roomsReceived = false;
currentScoreId = 0;
requestComplete = false;
bindHandler();
});
[Test]
public void TestShowResultsWithScore()
public void TestShowWithUserScore()
{
createResults(new TestScoreInfo(new OsuRuleset().RulesetInfo));
AddWaitStep("wait for display", 5);
ScoreInfo userScore = null;
AddStep("bind user score info handler", () =>
{
userScore = new TestScoreInfo(new OsuRuleset().RulesetInfo) { OnlineScoreID = currentScoreId++ };
bindHandler(userScore: userScore);
});
createResults(() => userScore);
waitForDisplay();
AddAssert("user score selected", () => this.ChildrenOfType<ScorePanel>().Single(p => p.Score.OnlineScoreID == userScore.OnlineScoreID).State == PanelState.Expanded);
}
[Test]
public void TestShowResultsNullScore()
public void TestShowNullUserScore()
{
createResults(null);
AddWaitStep("wait for display", 5);
createResults();
waitForDisplay();
AddAssert("top score selected", () => this.ChildrenOfType<ScorePanel>().OrderByDescending(p => p.Score.TotalScore).First().State == PanelState.Expanded);
}
[Test]
public void TestShowResultsNullScoreWithDelay()
public void TestShowUserScoreWithDelay()
{
ScoreInfo userScore = null;
AddStep("bind user score info handler", () =>
{
userScore = new TestScoreInfo(new OsuRuleset().RulesetInfo) { OnlineScoreID = currentScoreId++ };
bindHandler(3000, userScore);
});
createResults(() => userScore);
waitForDisplay();
AddAssert("more than 1 panel displayed", () => this.ChildrenOfType<ScorePanel>().Count() > 1);
AddAssert("user score selected", () => this.ChildrenOfType<ScorePanel>().Single(p => p.Score.OnlineScoreID == userScore.OnlineScoreID).State == PanelState.Expanded);
}
[Test]
public void TestShowNullUserScoreWithDelay()
{
AddStep("bind delayed handler", () => bindHandler(3000));
createResults(null);
AddUntilStep("wait for rooms to be received", () => roomsReceived);
AddWaitStep("wait for display", 5);
createResults();
waitForDisplay();
AddAssert("top score selected", () => this.ChildrenOfType<ScorePanel>().OrderByDescending(p => p.Score.TotalScore).First().State == PanelState.Expanded);
}
private void createResults(ScoreInfo score)
[Test]
public void TestFetchWhenScrolledToTheRight()
{
createResults();
waitForDisplay();
AddStep("bind delayed handler", () => bindHandler(3000));
for (int i = 0; i < 2; i++)
{
int beforePanelCount = 0;
AddStep("get panel count", () => beforePanelCount = this.ChildrenOfType<ScorePanel>().Count());
AddStep("scroll to right", () => resultsScreen.ScorePanelList.ChildrenOfType<OsuScrollContainer>().Single().ScrollToEnd(false));
AddAssert("right loading spinner shown", () => resultsScreen.RightSpinner.State.Value == Visibility.Visible);
waitForDisplay();
AddAssert($"count increased by {scores_per_result}", () => this.ChildrenOfType<ScorePanel>().Count() == beforePanelCount + scores_per_result);
AddAssert("right loading spinner hidden", () => resultsScreen.RightSpinner.State.Value == Visibility.Hidden);
}
}
[Test]
public void TestFetchWhenScrolledToTheLeft()
{
ScoreInfo userScore = null;
AddStep("bind user score info handler", () =>
{
userScore = new TestScoreInfo(new OsuRuleset().RulesetInfo) { OnlineScoreID = currentScoreId++ };
bindHandler(userScore: userScore);
});
createResults(() => userScore);
waitForDisplay();
AddStep("bind delayed handler", () => bindHandler(3000));
for (int i = 0; i < 2; i++)
{
int beforePanelCount = 0;
AddStep("get panel count", () => beforePanelCount = this.ChildrenOfType<ScorePanel>().Count());
AddStep("scroll to left", () => resultsScreen.ScorePanelList.ChildrenOfType<OsuScrollContainer>().Single().ScrollToStart(false));
AddAssert("left loading spinner shown", () => resultsScreen.LeftSpinner.State.Value == Visibility.Visible);
waitForDisplay();
AddAssert($"count increased by {scores_per_result}", () => this.ChildrenOfType<ScorePanel>().Count() == beforePanelCount + scores_per_result);
AddAssert("left loading spinner hidden", () => resultsScreen.LeftSpinner.State.Value == Visibility.Hidden);
}
}
private void createResults(Func<ScoreInfo> getScore = null)
{
AddStep("load results", () =>
{
LoadScreen(new TimeshiftResultsScreen(score, 1, new PlaylistItem
LoadScreen(resultsScreen = new TestResultsScreen(getScore?.Invoke(), 1, new PlaylistItem
{
Beatmap = { Value = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo },
Ruleset = { Value = new OsuRuleset().RulesetInfo }
@ -62,62 +163,213 @@ namespace osu.Game.Tests.Visual.Multiplayer
});
}
private void bindHandler(double delay = 0)
private void waitForDisplay()
{
var roomScores = new List<MultiplayerScore>();
AddUntilStep("wait for request to complete", () => requestComplete);
AddWaitStep("wait for display", 5);
}
for (int i = 0; i < 10; i++)
private void bindHandler(double delay = 0, ScoreInfo userScore = null, bool failRequests = false) => ((DummyAPIAccess)API).HandleRequest = request =>
{
requestComplete = false;
if (failRequests)
{
roomScores.Add(new MultiplayerScore
triggerFail(request, delay);
return;
}
switch (request)
{
case ShowPlaylistUserScoreRequest s:
if (userScore == null)
triggerFail(s, delay);
else
triggerSuccess(s, createUserResponse(userScore), delay);
break;
case IndexPlaylistScoresRequest i:
triggerSuccess(i, createIndexResponse(i), delay);
break;
}
};
private void triggerSuccess<T>(APIRequest<T> req, T result, double delay)
where T : class
{
if (delay == 0)
success();
else
{
Task.Run(async () =>
{
ID = i,
Accuracy = 0.9 - 0.01 * i,
EndedAt = DateTimeOffset.Now.Subtract(TimeSpan.FromHours(i)),
await Task.Delay(TimeSpan.FromMilliseconds(delay));
Schedule(success);
});
}
void success()
{
requestComplete = true;
req.TriggerSuccess(result);
}
}
private void triggerFail(APIRequest req, double delay)
{
if (delay == 0)
fail();
else
{
Task.Run(async () =>
{
await Task.Delay(TimeSpan.FromMilliseconds(delay));
Schedule(fail);
});
}
void fail()
{
requestComplete = true;
req.TriggerFailure(new WebException("Failed."));
}
}
private MultiplayerScore createUserResponse([NotNull] ScoreInfo userScore)
{
var multiplayerUserScore = new MultiplayerScore
{
ID = (int)(userScore.OnlineScoreID ?? currentScoreId++),
Accuracy = userScore.Accuracy,
EndedAt = userScore.Date,
Passed = userScore.Passed,
Rank = userScore.Rank,
MaxCombo = userScore.MaxCombo,
TotalScore = userScore.TotalScore,
User = userScore.User,
Statistics = userScore.Statistics,
ScoresAround = new MultiplayerScoresAround
{
Higher = new MultiplayerScores(),
Lower = new MultiplayerScores()
}
};
for (int i = 1; i <= scores_per_result; i++)
{
multiplayerUserScore.ScoresAround.Lower.Scores.Add(new MultiplayerScore
{
ID = currentScoreId++,
Accuracy = userScore.Accuracy,
EndedAt = userScore.Date,
Passed = true,
Rank = ScoreRank.B,
MaxCombo = 999,
TotalScore = 999999 - i * 1000,
Rank = userScore.Rank,
MaxCombo = userScore.MaxCombo,
TotalScore = userScore.TotalScore - i,
User = new User
{
Id = 2,
Username = $"peppy{i}",
CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
},
Statistics =
Statistics = userScore.Statistics
});
multiplayerUserScore.ScoresAround.Higher.Scores.Add(new MultiplayerScore
{
ID = currentScoreId++,
Accuracy = userScore.Accuracy,
EndedAt = userScore.Date,
Passed = true,
Rank = userScore.Rank,
MaxCombo = userScore.MaxCombo,
TotalScore = userScore.TotalScore + i,
User = new User
{
Id = 2,
Username = $"peppy{i}",
CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
},
Statistics = userScore.Statistics
});
}
addCursor(multiplayerUserScore.ScoresAround.Lower);
addCursor(multiplayerUserScore.ScoresAround.Higher);
return multiplayerUserScore;
}
private IndexedMultiplayerScores createIndexResponse(IndexPlaylistScoresRequest req)
{
var result = new IndexedMultiplayerScores();
long startTotalScore = req.Cursor?.Properties["total_score"].ToObject<long>() ?? 1000000;
string sort = req.IndexParams?.Properties["sort"].ToObject<string>() ?? "score_desc";
for (int i = 1; i <= scores_per_result; i++)
{
result.Scores.Add(new MultiplayerScore
{
ID = currentScoreId++,
Accuracy = 1,
EndedAt = DateTimeOffset.Now,
Passed = true,
Rank = ScoreRank.X,
MaxCombo = 1000,
TotalScore = startTotalScore + (sort == "score_asc" ? i : -i),
User = new User
{
Id = 2,
Username = $"peppy{i}",
CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
},
Statistics = new Dictionary<HitResult, int>
{
{ HitResult.Miss, 1 },
{ HitResult.Meh, 50 },
{ HitResult.Good, 100 },
{ HitResult.Great, 300 },
{ HitResult.Great, 300 }
}
});
}
((DummyAPIAccess)API).HandleRequest = request =>
addCursor(result);
return result;
}
private void addCursor(MultiplayerScores scores)
{
scores.Cursor = new Cursor
{
switch (request)
Properties = new Dictionary<string, JToken>
{
case GetRoomPlaylistScoresRequest r:
if (delay == 0)
success();
else
{
Task.Run(async () =>
{
await Task.Delay(TimeSpan.FromMilliseconds(delay));
Schedule(success);
});
}
{ "total_score", JToken.FromObject(scores.Scores[^1].TotalScore) },
{ "score_id", JToken.FromObject(scores.Scores[^1].ID) },
}
};
void success()
{
r.TriggerSuccess(new RoomPlaylistScores { Scores = roomScores });
roomsReceived = true;
}
break;
scores.Params = new IndexScoresParams
{
Properties = new Dictionary<string, JToken>
{
{ "sort", JToken.FromObject(scores.Scores[^1].TotalScore > scores.Scores[^2].TotalScore ? "score_asc" : "score_desc") }
}
};
}
private class TestResultsScreen : TimeshiftResultsScreen
{
public new LoadingSpinner LeftSpinner => base.LeftSpinner;
public new LoadingSpinner CentreSpinner => base.CentreSpinner;
public new LoadingSpinner RightSpinner => base.RightSpinner;
public new ScorePanelList ScorePanelList => base.ScorePanelList;
public TestResultsScreen(ScoreInfo score, int roomId, PlaylistItem playlistItem, bool allowRetry = true)
: base(score, roomId, playlistItem, allowRetry)
{
}
}
}
}

View File

@ -136,6 +136,11 @@ namespace osu.Game.Online.API
Success?.Invoke();
}
internal void TriggerFailure(Exception e)
{
Failure?.Invoke(e);
}
public void Cancel() => Fail(new OperationCanceledException(@"Request cancelled"));
public void Fail(Exception e)
@ -166,7 +171,7 @@ namespace osu.Game.Online.API
}
Logger.Log($@"Failing request {this} ({e})", LoggingTarget.Network);
pendingFailure = () => Failure?.Invoke(e);
pendingFailure = () => TriggerFailure(e);
checkAndScheduleFailure();
}

View File

@ -15,6 +15,6 @@ namespace osu.Game.Online.API.Requests
{
[UsedImplicitly]
[JsonExtensionData]
public IDictionary<string, JToken> Properties;
public IDictionary<string, JToken> Properties { get; set; } = new Dictionary<string, JToken>();
}
}

View File

@ -1,29 +0,0 @@
// 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.Collections.Generic;
using Newtonsoft.Json;
using osu.Game.Online.API;
namespace osu.Game.Online.Multiplayer
{
public class GetRoomPlaylistScoresRequest : APIRequest<RoomPlaylistScores>
{
private readonly int roomId;
private readonly int playlistItemId;
public GetRoomPlaylistScoresRequest(int roomId, int playlistItemId)
{
this.roomId = roomId;
this.playlistItemId = playlistItemId;
}
protected override string Target => $@"rooms/{roomId}/playlist/{playlistItemId}/scores";
}
public class RoomPlaylistScores
{
[JsonProperty("scores")]
public List<MultiplayerScore> Scores { get; set; }
}
}

View File

@ -0,0 +1,59 @@
// 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.Diagnostics;
using JetBrains.Annotations;
using osu.Framework.IO.Network;
using osu.Game.Extensions;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
namespace osu.Game.Online.Multiplayer
{
/// <summary>
/// Returns a list of scores for the specified playlist item.
/// </summary>
public class IndexPlaylistScoresRequest : APIRequest<IndexedMultiplayerScores>
{
public readonly int RoomId;
public readonly int PlaylistItemId;
[CanBeNull]
public readonly Cursor Cursor;
[CanBeNull]
public readonly IndexScoresParams IndexParams;
public IndexPlaylistScoresRequest(int roomId, int playlistItemId)
{
RoomId = roomId;
PlaylistItemId = playlistItemId;
}
public IndexPlaylistScoresRequest(int roomId, int playlistItemId, [NotNull] Cursor cursor, [NotNull] IndexScoresParams indexParams)
: this(roomId, playlistItemId)
{
Cursor = cursor;
IndexParams = indexParams;
}
protected override WebRequest CreateWebRequest()
{
var req = base.CreateWebRequest();
if (Cursor != null)
{
Debug.Assert(IndexParams != null);
req.AddCursor(Cursor);
foreach (var (key, value) in IndexParams.Properties)
req.AddParameter(key, value.ToString());
}
return req;
}
protected override string Target => $@"rooms/{RoomId}/playlist/{PlaylistItemId}/scores";
}
}

View File

@ -0,0 +1,20 @@
// 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.Collections.Generic;
using JetBrains.Annotations;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace osu.Game.Online.Multiplayer
{
/// <summary>
/// A collection of parameters which should be passed to the index endpoint to fetch the next page.
/// </summary>
public class IndexScoresParams
{
[UsedImplicitly]
[JsonExtensionData]
public IDictionary<string, JToken> Properties { get; set; } = new Dictionary<string, JToken>();
}
}

View File

@ -0,0 +1,27 @@
// 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 JetBrains.Annotations;
using Newtonsoft.Json;
namespace osu.Game.Online.Multiplayer
{
/// <summary>
/// A <see cref="MultiplayerScores"/> object returned via a <see cref="IndexPlaylistScoresRequest"/>.
/// </summary>
public class IndexedMultiplayerScores : MultiplayerScores
{
/// <summary>
/// The total scores in the playlist item.
/// </summary>
[JsonProperty("total")]
public int? TotalScores { get; set; }
/// <summary>
/// The user's score, if any.
/// </summary>
[JsonProperty("user_score")]
[CanBeNull]
public MultiplayerScore UserScore { get; set; }
}
}

View File

@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using JetBrains.Annotations;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using osu.Game.Online.API;
@ -47,6 +48,19 @@ namespace osu.Game.Online.Multiplayer
[JsonProperty("ended_at")]
public DateTimeOffset EndedAt { get; set; }
/// <summary>
/// The position of this score, starting at 1.
/// </summary>
[JsonProperty("position")]
public int? Position { get; set; }
/// <summary>
/// Any scores in the room around this score.
/// </summary>
[JsonProperty("scores_around")]
[CanBeNull]
public MultiplayerScoresAround ScoresAround { get; set; }
public ScoreInfo CreateScoreInfo(PlaylistItem playlistItem)
{
var rulesetInstance = playlistItem.Ruleset.Value.CreateInstance();

View File

@ -0,0 +1,27 @@
// 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.Collections.Generic;
using Newtonsoft.Json;
using osu.Game.Online.API.Requests;
namespace osu.Game.Online.Multiplayer
{
/// <summary>
/// An object which contains scores and related data for fetching next pages.
/// </summary>
public class MultiplayerScores : ResponseWithCursor
{
/// <summary>
/// The scores.
/// </summary>
[JsonProperty("scores")]
public List<MultiplayerScore> Scores { get; set; } = new List<MultiplayerScore>();
/// <summary>
/// The parameters to be used to fetch the next page.
/// </summary>
[JsonProperty("params")]
public IndexScoresParams Params { get; set; } = new IndexScoresParams();
}
}

View File

@ -0,0 +1,28 @@
// 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 JetBrains.Annotations;
using Newtonsoft.Json;
namespace osu.Game.Online.Multiplayer
{
/// <summary>
/// An object which stores scores higher and lower than the user's score.
/// </summary>
public class MultiplayerScoresAround
{
/// <summary>
/// Scores sorted "higher" than the user's score, depending on the sorting order.
/// </summary>
[JsonProperty("higher")]
[CanBeNull]
public MultiplayerScores Higher { get; set; }
/// <summary>
/// Scores sorted "lower" than the user's score, depending on the sorting order.
/// </summary>
[JsonProperty("lower")]
[CanBeNull]
public MultiplayerScores Lower { get; set; }
}
}

View File

@ -0,0 +1,23 @@
// 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 osu.Game.Online.API;
namespace osu.Game.Online.Multiplayer
{
public class ShowPlaylistUserScoreRequest : APIRequest<MultiplayerScore>
{
private readonly int roomId;
private readonly int playlistItemId;
private readonly long userId;
public ShowPlaylistUserScoreRequest(int roomId, int playlistItemId, long userId)
{
this.roomId = roomId;
this.playlistItemId = playlistItemId;
this.userId = userId;
}
protected override string Target => $"rooms/{roomId}/playlist/{playlistItemId}/scores/users/{userId}";
}
}

View File

@ -3,7 +3,9 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@ -20,7 +22,15 @@ namespace osu.Game.Screens.Multi.Ranking
private readonly int roomId;
private readonly PlaylistItem playlistItem;
private LoadingSpinner loadingLayer;
protected LoadingSpinner LeftSpinner { get; private set; }
protected LoadingSpinner CentreSpinner { get; private set; }
protected LoadingSpinner RightSpinner { get; private set; }
private MultiplayerScores higherScores;
private MultiplayerScores lowerScores;
[Resolved]
private IAPIProvider api { get; set; }
public TimeshiftResultsScreen(ScoreInfo score, int roomId, PlaylistItem playlistItem, bool allowRetry = true)
: base(score, allowRetry)
@ -32,29 +42,173 @@ namespace osu.Game.Screens.Multi.Ranking
[BackgroundDependencyLoader]
private void load()
{
AddInternal(loadingLayer = new LoadingLayer
AddInternal(new Container
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
X = -10,
State = { Value = Score == null ? Visibility.Visible : Visibility.Hidden },
Padding = new MarginPadding { Bottom = TwoLayerButton.SIZE_EXTENDED.Y }
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Bottom = TwoLayerButton.SIZE_EXTENDED.Y },
Children = new Drawable[]
{
LeftSpinner = new PanelListLoadingSpinner(ScorePanelList)
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.Centre,
},
CentreSpinner = new PanelListLoadingSpinner(ScorePanelList)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
State = { Value = Score == null ? Visibility.Visible : Visibility.Hidden },
},
RightSpinner = new PanelListLoadingSpinner(ScorePanelList)
{
Anchor = Anchor.CentreRight,
Origin = Anchor.Centre,
},
}
});
}
protected override APIRequest FetchScores(Action<IEnumerable<ScoreInfo>> scoresCallback)
{
var req = new GetRoomPlaylistScoresRequest(roomId, playlistItem.ID);
// This performs two requests:
// 1. A request to show the user's score (and scores around).
// 2. If that fails, a request to index the room starting from the highest score.
req.Success += r =>
var userScoreReq = new ShowPlaylistUserScoreRequest(roomId, playlistItem.ID, api.LocalUser.Value.Id);
userScoreReq.Success += userScore =>
{
scoresCallback?.Invoke(r.Scores.Where(s => s.ID != Score?.OnlineScoreID).Select(s => s.CreateScoreInfo(playlistItem)));
loadingLayer.Hide();
var allScores = new List<MultiplayerScore> { userScore };
if (userScore.ScoresAround?.Higher != null)
{
allScores.AddRange(userScore.ScoresAround.Higher.Scores);
higherScores = userScore.ScoresAround.Higher;
}
if (userScore.ScoresAround?.Lower != null)
{
allScores.AddRange(userScore.ScoresAround.Lower.Scores);
lowerScores = userScore.ScoresAround.Lower;
}
performSuccessCallback(scoresCallback, allScores);
};
req.Failure += _ => loadingLayer.Hide();
// On failure, fallback to a normal index.
userScoreReq.Failure += _ => api.Queue(createIndexRequest(scoresCallback));
return req;
return userScoreReq;
}
protected override APIRequest FetchNextPage(int direction, Action<IEnumerable<ScoreInfo>> scoresCallback)
{
Debug.Assert(direction == 1 || direction == -1);
MultiplayerScores pivot = direction == -1 ? higherScores : lowerScores;
if (pivot?.Cursor == null)
return null;
if (pivot == higherScores)
LeftSpinner.Show();
else
RightSpinner.Show();
return createIndexRequest(scoresCallback, pivot);
}
/// <summary>
/// Creates a <see cref="IndexPlaylistScoresRequest"/> with an optional score pivot.
/// </summary>
/// <remarks>Does not queue the request.</remarks>
/// <param name="scoresCallback">The callback to perform with the resulting scores.</param>
/// <param name="pivot">An optional score pivot to retrieve scores around. Can be null to retrieve scores from the highest score.</param>
/// <returns>The indexing <see cref="APIRequest"/>.</returns>
private APIRequest createIndexRequest(Action<IEnumerable<ScoreInfo>> scoresCallback, [CanBeNull] MultiplayerScores pivot = null)
{
var indexReq = pivot != null
? new IndexPlaylistScoresRequest(roomId, playlistItem.ID, pivot.Cursor, pivot.Params)
: new IndexPlaylistScoresRequest(roomId, playlistItem.ID);
indexReq.Success += r =>
{
if (pivot == lowerScores)
lowerScores = r;
else
higherScores = r;
performSuccessCallback(scoresCallback, r.Scores, r);
};
indexReq.Failure += _ => hideLoadingSpinners(pivot);
return indexReq;
}
/// <summary>
/// Transforms returned <see cref="MultiplayerScores"/> into <see cref="ScoreInfo"/>s, ensure the <see cref="ScorePanelList"/> is put into a sane state, and invokes a given success callback.
/// </summary>
/// <param name="callback">The callback to invoke with the final <see cref="ScoreInfo"/>s.</param>
/// <param name="scores">The <see cref="MultiplayerScore"/>s that were retrieved from <see cref="APIRequest"/>s.</param>
/// <param name="pivot">An optional pivot around which the scores were retrieved.</param>
private void performSuccessCallback([NotNull] Action<IEnumerable<ScoreInfo>> callback, [NotNull] List<MultiplayerScore> scores, [CanBeNull] MultiplayerScores pivot = null)
{
var scoreInfos = new List<ScoreInfo>(scores.Select(s => s.CreateScoreInfo(playlistItem)));
// Select a score if we don't already have one selected.
// Note: This is done before the callback so that the panel list centres on the selected score before panels are added (eliminating initial scroll).
if (SelectedScore.Value == null)
{
Schedule(() =>
{
// Prefer selecting the local user's score, or otherwise default to the first visible score.
SelectedScore.Value = scoreInfos.FirstOrDefault(s => s.User.Id == api.LocalUser.Value.Id) ?? scoreInfos.FirstOrDefault();
});
}
// Invoke callback to add the scores. Exclude the user's current score which was added previously.
callback.Invoke(scoreInfos.Where(s => s.OnlineScoreID != Score?.OnlineScoreID));
hideLoadingSpinners(pivot);
}
private void hideLoadingSpinners([CanBeNull] MultiplayerScores pivot = null)
{
CentreSpinner.Hide();
if (pivot == lowerScores)
RightSpinner.Hide();
else if (pivot == higherScores)
LeftSpinner.Hide();
}
private class PanelListLoadingSpinner : LoadingSpinner
{
private readonly ScorePanelList list;
/// <summary>
/// Creates a new <see cref="PanelListLoadingSpinner"/>.
/// </summary>
/// <param name="list">The list to track.</param>
/// <param name="withBox">Whether the spinner should have a surrounding black box for visibility.</param>
public PanelListLoadingSpinner(ScorePanelList list, bool withBox = true)
: base(withBox)
{
this.list = list;
}
protected override void Update()
{
base.Update();
float panelOffset = list.DrawWidth / 2 - ScorePanel.EXPANDED_WIDTH;
if ((Anchor & Anchor.x0) > 0)
X = (float)(panelOffset - list.Current);
else if ((Anchor & Anchor.x2) > 0)
X = (float)(list.ScrollableExtent - list.Current - panelOffset);
}
}
}
}

View File

@ -37,7 +37,8 @@ namespace osu.Game.Screens.Ranking
public readonly Bindable<ScoreInfo> SelectedScore = new Bindable<ScoreInfo>();
public readonly ScoreInfo Score;
private readonly bool allowRetry;
protected ScorePanelList ScorePanelList { get; private set; }
[Resolved(CanBeNull = true)]
private Player player { get; set; }
@ -47,9 +48,13 @@ namespace osu.Game.Screens.Ranking
private StatisticsPanel statisticsPanel;
private Drawable bottomPanel;
private ScorePanelList scorePanelList;
private Container<ScorePanel> detachedPanelContainer;
private bool fetchedInitialScores;
private APIRequest nextPageRequest;
private readonly bool allowRetry;
protected ResultsScreen(ScoreInfo score, bool allowRetry = true)
{
Score = score;
@ -84,7 +89,7 @@ namespace osu.Game.Screens.Ranking
RelativeSizeAxes = Axes.Both,
Score = { BindTarget = SelectedScore }
},
scorePanelList = new ScorePanelList
ScorePanelList = new ScorePanelList
{
RelativeSizeAxes = Axes.Both,
SelectedScore = { BindTarget = SelectedScore },
@ -142,7 +147,7 @@ namespace osu.Game.Screens.Ranking
};
if (Score != null)
scorePanelList.AddScore(Score);
ScorePanelList.AddScore(Score);
if (player != null && allowRetry)
{
@ -164,11 +169,7 @@ namespace osu.Game.Screens.Ranking
{
base.LoadComplete();
var req = FetchScores(scores => Schedule(() =>
{
foreach (var s in scores)
addScore(s);
}));
var req = FetchScores(fetchScoresCallback);
if (req != null)
api.Queue(req);
@ -176,6 +177,28 @@ namespace osu.Game.Screens.Ranking
statisticsPanel.State.BindValueChanged(onStatisticsStateChanged, true);
}
protected override void Update()
{
base.Update();
if (fetchedInitialScores && nextPageRequest == null)
{
if (ScorePanelList.IsScrolledToStart)
nextPageRequest = FetchNextPage(-1, fetchScoresCallback);
else if (ScorePanelList.IsScrolledToEnd)
nextPageRequest = FetchNextPage(1, fetchScoresCallback);
if (nextPageRequest != null)
{
// Scheduled after children to give the list a chance to update its scroll position and not potentially trigger a second request too early.
nextPageRequest.Success += () => ScheduleAfterChildren(() => nextPageRequest = null);
nextPageRequest.Failure += _ => ScheduleAfterChildren(() => nextPageRequest = null);
api.Queue(nextPageRequest);
}
}
}
/// <summary>
/// Performs a fetch/refresh of scores to be displayed.
/// </summary>
@ -183,6 +206,22 @@ namespace osu.Game.Screens.Ranking
/// <returns>An <see cref="APIRequest"/> responsible for the fetch operation. This will be queued and performed automatically.</returns>
protected virtual APIRequest FetchScores(Action<IEnumerable<ScoreInfo>> scoresCallback) => null;
/// <summary>
/// Performs a fetch of the next page of scores. This is invoked every frame until a non-null <see cref="APIRequest"/> is returned.
/// </summary>
/// <param name="direction">The fetch direction. -1 to fetch scores greater than the current start of the list, and 1 to fetch scores lower than the current end of the list.</param>
/// <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 virtual APIRequest FetchNextPage(int direction, Action<IEnumerable<ScoreInfo>> scoresCallback) => null;
private void fetchScoresCallback(IEnumerable<ScoreInfo> scores) => Schedule(() =>
{
foreach (var s in scores)
addScore(s);
fetchedInitialScores = true;
});
public override void OnEntering(IScreen last)
{
base.OnEntering(last);
@ -213,7 +252,7 @@ namespace osu.Game.Screens.Ranking
private void addScore(ScoreInfo score)
{
var panel = scorePanelList.AddScore(score);
var panel = ScorePanelList.AddScore(score);
if (detachedPanel != null)
panel.Alpha = 0;
@ -226,11 +265,11 @@ namespace osu.Game.Screens.Ranking
if (state.NewValue == Visibility.Visible)
{
// Detach the panel in its original location, and move into the desired location in the local container.
var expandedPanel = scorePanelList.GetPanelForScore(SelectedScore.Value);
var expandedPanel = ScorePanelList.GetPanelForScore(SelectedScore.Value);
var screenSpacePos = expandedPanel.ScreenSpaceDrawQuad.TopLeft;
// Detach and move into the local container.
scorePanelList.Detach(expandedPanel);
ScorePanelList.Detach(expandedPanel);
detachedPanelContainer.Add(expandedPanel);
// Move into its original location in the local container first, then to the final location.
@ -240,9 +279,9 @@ namespace osu.Game.Screens.Ranking
.MoveTo(new Vector2(StatisticsPanel.SIDE_PADDING, origLocation.Y), 150, Easing.OutQuint);
// Hide contracted panels.
foreach (var contracted in scorePanelList.GetScorePanels().Where(p => p.State == PanelState.Contracted))
foreach (var contracted in ScorePanelList.GetScorePanels().Where(p => p.State == PanelState.Contracted))
contracted.FadeOut(150, Easing.OutQuint);
scorePanelList.HandleInput = false;
ScorePanelList.HandleInput = false;
// Dim background.
Background.FadeTo(0.1f, 150);
@ -255,7 +294,7 @@ namespace osu.Game.Screens.Ranking
// Remove from the local container and re-attach.
detachedPanelContainer.Remove(detachedPanel);
scorePanelList.Attach(detachedPanel);
ScorePanelList.Attach(detachedPanel);
// Move into its original location in the attached container first, then to the final location.
var origLocation = detachedPanel.Parent.ToLocalSpace(screenSpacePos);
@ -264,9 +303,9 @@ namespace osu.Game.Screens.Ranking
.MoveTo(new Vector2(0, origLocation.Y), 150, Easing.OutQuint);
// Show contracted panels.
foreach (var contracted in scorePanelList.GetScorePanels().Where(p => p.State == PanelState.Contracted))
foreach (var contracted in ScorePanelList.GetScorePanels().Where(p => p.State == PanelState.Contracted))
contracted.FadeIn(150, Easing.OutQuint);
scorePanelList.HandleInput = true;
ScorePanelList.HandleInput = true;
// Un-dim background.
Background.FadeTo(0.5f, 150);

View File

@ -26,6 +26,31 @@ namespace osu.Game.Screens.Ranking
/// </summary>
private const float expanded_panel_spacing = 15;
/// <summary>
/// Minimum distance from either end point of the list that the list can be considered scrolled to the end point.
/// </summary>
private const float scroll_endpoint_distance = 100;
/// <summary>
/// Whether this <see cref="ScorePanelList"/> can be scrolled and is currently scrolled to the start.
/// </summary>
public bool IsScrolledToStart => flow.Count > 0 && scroll.ScrollableExtent > 0 && scroll.Current <= scroll_endpoint_distance;
/// <summary>
/// Whether this <see cref="ScorePanelList"/> can be scrolled and is currently scrolled to the end.
/// </summary>
public bool IsScrolledToEnd => flow.Count > 0 && scroll.ScrollableExtent > 0 && scroll.IsScrolledToEnd(scroll_endpoint_distance);
/// <summary>
/// The current scroll position.
/// </summary>
public double Current => scroll.Current;
/// <summary>
/// The scrollable extent.
/// </summary>
public double ScrollableExtent => scroll.ScrollableExtent;
/// <summary>
/// An action to be invoked if a <see cref="ScorePanel"/> is clicked while in an expanded state.
/// </summary>