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

Knock some sense into the playlists results screen implementation

As we're moving towards using the `/playlist/<id>/scores/<id>` endpoint,
the existing playlists results screen classes needed some restructuring.
This commit is contained in:
Salman Alshamrani 2024-11-27 00:55:02 -05:00
parent 9083daf363
commit dfbccc2144
9 changed files with 161 additions and 94 deletions

View File

@ -7,6 +7,7 @@ using System.Linq;
using System.Net;
using Newtonsoft.Json.Linq;
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Game.Graphics.Containers;
@ -31,7 +32,7 @@ namespace osu.Game.Tests.Visual.Playlists
private const int scores_per_result = 10;
private const int real_user_position = 200;
private TestResultsScreen resultsScreen = null!;
private ResultsScreen resultsScreen = null!;
private int lowestScoreId; // Score ID of the lowest score in the list.
private int highestScoreId; // Score ID of the highest score in the list.
@ -68,11 +69,11 @@ namespace osu.Game.Tests.Visual.Playlists
}
[Test]
public void TestShowWithUserScore()
public void TestShowUserScore()
{
AddStep("bind user score info handler", () => bindHandler(userScore: userScore));
createResults(() => userScore);
createResultsWithScore(() => userScore);
waitForDisplay();
AddAssert("user score selected", () => this.ChildrenOfType<ScorePanel>().Single(p => p.Score.OnlineID == userScore.OnlineID).State == PanelState.Expanded);
@ -81,11 +82,24 @@ namespace osu.Game.Tests.Visual.Playlists
}
[Test]
public void TestShowNullUserScore()
public void TestShowUserBest()
{
AddStep("bind user score info handler", () => bindHandler(userScore: userScore));
createUserBestResults();
waitForDisplay();
AddAssert("user score selected", () => this.ChildrenOfType<ScorePanel>().Single(p => p.Score.UserID == userScore.UserID).State == PanelState.Expanded);
AddAssert($"score panel position is {real_user_position}",
() => this.ChildrenOfType<ScorePanel>().Single(p => p.Score.UserID == userScore.UserID).ScorePosition.Value == real_user_position);
}
[Test]
public void TestShowNonUserScores()
{
AddStep("bind user score info handler", () => bindHandler());
createResults();
createUserBestResults();
waitForDisplay();
AddAssert("top score selected", () => this.ChildrenOfType<ScorePanel>().OrderByDescending(p => p.Score.TotalScore).First().State == PanelState.Expanded);
@ -96,7 +110,7 @@ namespace osu.Game.Tests.Visual.Playlists
{
AddStep("bind user score info handler", () => bindHandler(true, userScore));
createResults(() => userScore);
createResultsWithScore(() => userScore);
waitForDisplay();
AddAssert("more than 1 panel displayed", () => this.ChildrenOfType<ScorePanel>().Count() > 1);
@ -104,11 +118,11 @@ namespace osu.Game.Tests.Visual.Playlists
}
[Test]
public void TestShowNullUserScoreWithDelay()
public void TestShowNonUserScoresWithDelay()
{
AddStep("bind delayed handler", () => bindHandler(true));
createResults();
createUserBestResults();
waitForDisplay();
AddAssert("top score selected", () => this.ChildrenOfType<ScorePanel>().OrderByDescending(p => p.Score.TotalScore).First().State == PanelState.Expanded);
@ -119,7 +133,7 @@ namespace osu.Game.Tests.Visual.Playlists
{
AddStep("bind delayed handler", () => bindHandler(true));
createResults();
createUserBestResults();
waitForDisplay();
for (int i = 0; i < 2; i++)
@ -127,13 +141,16 @@ namespace osu.Game.Tests.Visual.Playlists
int beforePanelCount = 0;
AddStep("get panel count", () => beforePanelCount = this.ChildrenOfType<ScorePanel>().Count());
AddStep("scroll to right", () => resultsScreen.ScorePanelList.ChildrenOfType<OsuScrollContainer>().Single().ScrollToEnd(false));
AddStep("scroll to right", () => resultsScreen.ChildrenOfType<ScorePanelList>().Single().ChildrenOfType<OsuScrollContainer>().Single().ScrollToEnd(false));
AddAssert("right loading spinner shown", () =>
resultsScreen.ChildrenOfType<LoadingSpinner>().Single(l => l.Anchor == Anchor.CentreRight).State.Value == Visibility.Visible);
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);
AddAssert("right loading spinner hidden", () =>
resultsScreen.ChildrenOfType<LoadingSpinner>().Single(l => l.Anchor == Anchor.CentreRight).State.Value == Visibility.Hidden);
}
}
@ -142,29 +159,36 @@ namespace osu.Game.Tests.Visual.Playlists
{
AddStep("bind delayed handler with scores", () => bindHandler(delayed: true));
createResults();
createUserBestResults();
waitForDisplay();
int beforePanelCount = 0;
AddStep("get panel count", () => beforePanelCount = this.ChildrenOfType<ScorePanel>().Count());
AddStep("scroll to right", () => resultsScreen.ScorePanelList.ChildrenOfType<OsuScrollContainer>().Single().ScrollToEnd(false));
AddStep("scroll to right", () => resultsScreen.ChildrenOfType<ScorePanelList>().Single().ChildrenOfType<OsuScrollContainer>().Single().ScrollToEnd(false));
AddAssert("right loading spinner shown", () =>
resultsScreen.ChildrenOfType<LoadingSpinner>().Single(l => l.Anchor == Anchor.CentreRight).State.Value == Visibility.Visible);
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);
AddAssert("right loading spinner hidden", () =>
resultsScreen.ChildrenOfType<LoadingSpinner>().Single(l => l.Anchor == Anchor.CentreRight).State.Value == Visibility.Hidden);
AddStep("get panel count", () => beforePanelCount = this.ChildrenOfType<ScorePanel>().Count());
AddStep("bind delayed handler with no scores", () => bindHandler(delayed: true, noScores: true));
AddStep("scroll to right", () => resultsScreen.ScorePanelList.ChildrenOfType<OsuScrollContainer>().Single().ScrollToEnd(false));
AddStep("scroll to right", () => resultsScreen.ChildrenOfType<ScorePanelList>().Single().ChildrenOfType<OsuScrollContainer>().Single().ScrollToEnd(false));
AddAssert("right loading spinner shown", () =>
resultsScreen.ChildrenOfType<LoadingSpinner>().Single(l => l.Anchor == Anchor.CentreRight).State.Value == Visibility.Visible);
AddAssert("right loading spinner shown", () => resultsScreen.RightSpinner.State.Value == Visibility.Visible);
waitForDisplay();
AddAssert("count not increased", () => this.ChildrenOfType<ScorePanel>().Count() == beforePanelCount);
AddAssert("right loading spinner hidden", () => resultsScreen.RightSpinner.State.Value == Visibility.Hidden);
AddAssert("right loading spinner hidden", () =>
resultsScreen.ChildrenOfType<LoadingSpinner>().Single(l => l.Anchor == Anchor.CentreRight).State.Value == Visibility.Hidden);
AddAssert("no placeholders shown", () => this.ChildrenOfType<MessagePlaceholder>().Count(), () => Is.Zero);
}
@ -173,7 +197,7 @@ namespace osu.Game.Tests.Visual.Playlists
{
AddStep("bind user score info handler", () => bindHandler(userScore: userScore));
createResults(() => userScore);
createResultsWithScore(() => userScore);
waitForDisplay();
AddStep("bind delayed handler", () => bindHandler(true));
@ -183,30 +207,36 @@ namespace osu.Game.Tests.Visual.Playlists
int beforePanelCount = 0;
AddStep("get panel count", () => beforePanelCount = this.ChildrenOfType<ScorePanel>().Count());
AddStep("scroll to left", () => resultsScreen.ScorePanelList.ChildrenOfType<OsuScrollContainer>().Single().ScrollToStart(false));
AddStep("scroll to left", () => resultsScreen.ChildrenOfType<ScorePanelList>().Single().ChildrenOfType<OsuScrollContainer>().Single().ScrollToStart(false));
AddAssert("left loading spinner shown", () =>
resultsScreen.ChildrenOfType<LoadingSpinner>().Single(l => l.Anchor == Anchor.CentreLeft).State.Value == Visibility.Visible);
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);
AddAssert("left loading spinner hidden", () =>
resultsScreen.ChildrenOfType<LoadingSpinner>().Single(l => l.Anchor == Anchor.CentreLeft).State.Value == Visibility.Hidden);
}
}
/// <summary>
/// Shows the <see cref="TestUserBestResultsScreen"/> with no scores provided by the API.
/// </summary>
[Test]
public void TestShowWithNoScores()
public void TestShowUserBestWithNoScoresPresent()
{
AddStep("bind user score info handler", () => bindHandler(noScores: true));
createResults();
AddAssert("no scores visible", () => !resultsScreen.ScorePanelList.GetScorePanels().Any());
createUserBestResults();
AddAssert("no scores visible", () => !resultsScreen.ChildrenOfType<ScorePanelList>().Single().GetScorePanels().Any());
AddAssert("placeholder shown", () => this.ChildrenOfType<MessagePlaceholder>().Count(), () => Is.EqualTo(1));
}
private void createResults(Func<ScoreInfo>? getScore = null)
private void createResultsWithScore(Func<ScoreInfo> getScore)
{
AddStep("load results", () =>
{
LoadScreen(resultsScreen = new TestResultsScreen(getScore?.Invoke(), 1, new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo)
LoadScreen(resultsScreen = new TestScoreResultsScreen(getScore(), 1, new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo)
{
RulesetID = new OsuRuleset().RulesetInfo.OnlineID
}));
@ -215,14 +245,27 @@ namespace osu.Game.Tests.Visual.Playlists
AddUntilStep("wait for screen to load", () => resultsScreen.IsLoaded);
}
private void createUserBestResults()
{
AddStep("load results", () =>
{
LoadScreen(resultsScreen = new TestUserBestResultsScreen(1, new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo)
{
RulesetID = new OsuRuleset().RulesetInfo.OnlineID
}, 2));
});
AddUntilStep("wait for screen to load", () => resultsScreen.IsLoaded);
}
private void waitForDisplay()
{
AddUntilStep("wait for scores loaded", () =>
requestComplete
// request handler may need to fire more than once to get scores.
&& totalCount > 0
&& resultsScreen.ScorePanelList.GetScorePanels().Count() == totalCount
&& resultsScreen.ScorePanelList.AllPanelsVisible);
&& resultsScreen.ChildrenOfType<ScorePanelList>().Single().GetScorePanels().Count() == totalCount
&& resultsScreen.ChildrenOfType<ScorePanelList>().Single().AllPanelsVisible);
AddWaitStep("wait for display", 5);
}
@ -232,6 +275,7 @@ namespace osu.Game.Tests.Visual.Playlists
switch (request)
{
case ShowPlaylistScoreRequest:
case ShowPlaylistUserScoreRequest:
case IndexPlaylistScoresRequest:
break;
@ -261,6 +305,14 @@ namespace osu.Game.Tests.Visual.Playlists
break;
case ShowPlaylistUserScoreRequest u:
if (userScore == null)
triggerFail(u);
else
triggerSuccess(u, createUserResponse(userScore));
break;
case IndexPlaylistScoresRequest i:
triggerSuccess(i, createIndexResponse(i, noScores));
break;
@ -314,7 +366,7 @@ namespace osu.Game.Tests.Visual.Playlists
MaxCombo = userScore.MaxCombo,
User = new APIUser
{
Id = 2,
Id = 2 + i,
Username = $"peppy{i}",
CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
},
@ -329,7 +381,7 @@ namespace osu.Game.Tests.Visual.Playlists
MaxCombo = userScore.MaxCombo,
User = new APIUser
{
Id = 2,
Id = 2 + i,
Username = $"peppy{i}",
CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
},
@ -363,7 +415,7 @@ namespace osu.Game.Tests.Visual.Playlists
MaxCombo = 1000,
User = new APIUser
{
Id = 2,
Id = 2 + i,
Username = $"peppy{i}",
CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
},
@ -410,18 +462,32 @@ namespace osu.Game.Tests.Visual.Playlists
};
}
private partial class TestResultsScreen : PlaylistItemUserResultsScreen
private partial class TestScoreResultsScreen : PlaylistItemScoreResultsScreen
{
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)
public TestScoreResultsScreen(ScoreInfo score, int roomId, PlaylistItem playlistItem)
: base(score, roomId, playlistItem)
{
AllowRetry = true;
}
}
private partial class TestUserBestResultsScreen : PlaylistItemUserBestResultsScreen
{
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 TestUserBestResultsScreen(int roomId, PlaylistItem playlistItem, int userId)
: base(roomId, playlistItem, userId)
{
AllowRetry = true;
}
}
}
}

View File

@ -345,7 +345,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
private void presentScore(long id)
{
if (this.IsCurrentScreen())
this.Push(new PlaylistItemScoreResultsScreen(room.RoomID!.Value, playlistItem, id));
this.Push(new PlaylistItemScoreResultsScreen(id, room.RoomID!.Value, playlistItem));
}
private void onRoomScoreSet(MultiplayerRoomScoreSetEvent e)

View File

@ -7,7 +7,7 @@ using osu.Game.Screens.OnlinePlay.Playlists;
namespace osu.Game.Screens.OnlinePlay.Multiplayer
{
public partial class MultiplayerResultsScreen : PlaylistItemUserResultsScreen
public partial class MultiplayerResultsScreen : PlaylistItemScoreResultsScreen
{
public MultiplayerResultsScreen(ScoreInfo score, long roomId, PlaylistItem playlistItem)
: base(score, roomId, playlistItem)

View File

@ -191,8 +191,8 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
{
var scoreInfos = scores.Select(s => s.CreateScoreInfo(ScoreManager, Rulesets, PlaylistItem, Beatmap.Value.BeatmapInfo)).OrderByTotalScore().ToArray();
// Invoke callback to add the scores.
callback.Invoke(scoreInfos);
// Invoke callback to add the scores. Exclude the score provided to this screen since it's added already.
callback.Invoke(scoreInfos.Where(s => s.OnlineID != Score?.OnlineID));
return scoreInfos;
}

View File

@ -11,13 +11,19 @@ using osu.Game.Scoring;
namespace osu.Game.Screens.OnlinePlay.Playlists
{
/// <summary>
/// Shows a selected arbitrary score for a playlist item, with scores around included.
/// Shows a given score in a playlist item, with scores around included.
/// </summary>
public partial class PlaylistItemScoreResultsScreen : PlaylistItemResultsScreen
{
private readonly long scoreId;
public PlaylistItemScoreResultsScreen(long roomId, PlaylistItem playlistItem, long scoreId)
public PlaylistItemScoreResultsScreen(ScoreInfo score, long roomId, PlaylistItem playlistItem)
: base(score, roomId, playlistItem)
{
scoreId = score.OnlineID;
}
public PlaylistItemScoreResultsScreen(long scoreId, long roomId, PlaylistItem playlistItem)
: base(null, roomId, playlistItem)
{
this.scoreId = scoreId;
@ -28,9 +34,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
protected override ScoreInfo[] PerformSuccessCallback(Action<IEnumerable<ScoreInfo>> callback, List<MultiplayerScore> scores, MultiplayerScores? pivot = null)
{
var scoreInfos = base.PerformSuccessCallback(callback, scores, pivot);
Schedule(() => SelectedScore.Value ??= scoreInfos.SingleOrDefault(score => score.OnlineID == scoreId));
Schedule(() => SelectedScore.Value ??= scoreInfos.SingleOrDefault(s => s.OnlineID == scoreId));
return scoreInfos;
}
}

View File

@ -0,0 +1,41 @@
// 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.Linq;
using osu.Game.Online.API;
using osu.Game.Online.Rooms;
using osu.Game.Scoring;
namespace osu.Game.Screens.OnlinePlay.Playlists
{
/// <summary>
/// Shows a user's best score in a playlist item, with scores around included.
/// </summary>
public partial class PlaylistItemUserBestResultsScreen : PlaylistItemResultsScreen
{
private readonly int userId;
public PlaylistItemUserBestResultsScreen(long roomId, PlaylistItem playlistItem, int userId)
: base(null, roomId, playlistItem)
{
this.userId = userId;
}
protected override APIRequest<MultiplayerScore> CreateScoreRequest() => new ShowPlaylistUserScoreRequest(RoomId, PlaylistItem.ID, userId);
protected override ScoreInfo[] PerformSuccessCallback(Action<IEnumerable<ScoreInfo>> callback, List<MultiplayerScore> scores, MultiplayerScores? pivot = null)
{
var scoreInfos = base.PerformSuccessCallback(callback, scores, pivot);
Schedule(() =>
{
// Prefer selecting the local user's score, or otherwise default to the first visible score.
SelectedScore.Value ??= scoreInfos.FirstOrDefault(s => s.UserID == userId) ?? scoreInfos.FirstOrDefault();
});
return scoreInfos;
}
}
}

View File

@ -1,48 +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;
using System.Collections.Generic;
using System.Linq;
using osu.Game.Online.API;
using osu.Game.Online.Rooms;
using osu.Game.Scoring;
namespace osu.Game.Screens.OnlinePlay.Playlists
{
/// <summary>
/// Shows the user's submitted score in a given playlist item, with scores around included.
/// </summary>
public partial class PlaylistItemUserResultsScreen : PlaylistItemResultsScreen
{
public PlaylistItemUserResultsScreen(ScoreInfo? score, long roomId, PlaylistItem playlistItem)
: base(score, roomId, playlistItem)
{
}
protected override APIRequest<MultiplayerScore> CreateScoreRequest() => Score != null
? new ShowPlaylistScoreRequest(RoomId, PlaylistItem.ID, Score.OnlineID)
: new ShowPlaylistUserScoreRequest(RoomId, PlaylistItem.ID, API.LocalUser.Value.Id);
protected override ScoreInfo[] PerformSuccessCallback(Action<IEnumerable<ScoreInfo>> callback, List<MultiplayerScore> scores, MultiplayerScores? pivot = null)
{
var scoreInfos = scores.Select(s => s.CreateScoreInfo(ScoreManager, Rulesets, PlaylistItem, Beatmap.Value.BeatmapInfo)).OrderByTotalScore().ToArray();
// 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.OnlineID == 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.OnlineID != Score?.OnlineID));
return scoreInfos;
}
}
}

View File

@ -56,7 +56,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
protected override ResultsScreen CreateResults(ScoreInfo score)
{
Debug.Assert(Room.RoomID != null);
return new PlaylistItemUserResultsScreen(score, Room.RoomID.Value, PlaylistItem)
return new PlaylistItemScoreResultsScreen(score, Room.RoomID.Value, PlaylistItem)
{
AllowRetry = true,
ShowUserStatistics = true,

View File

@ -12,6 +12,7 @@ using osu.Framework.Logging;
using osu.Framework.Screens;
using osu.Game.Graphics.Cursor;
using osu.Game.Input;
using osu.Game.Online.API;
using osu.Game.Online.Rooms;
using osu.Game.Screens.OnlinePlay.Components;
using osu.Game.Screens.OnlinePlay.Match;
@ -32,6 +33,9 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
private readonly IBindable<bool> isIdle = new BindableBool();
[Resolved]
private IAPIProvider api { get; set; } = null!;
[Resolved(CanBeNull = true)]
private IdleTracker? idleTracker { get; set; }
@ -143,7 +147,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
RequestResults = item =>
{
Debug.Assert(Room.RoomID != null);
ParentScreen?.Push(new PlaylistItemUserResultsScreen(null, Room.RoomID.Value, item));
ParentScreen?.Push(new PlaylistItemUserBestResultsScreen(Room.RoomID.Value, item, api.LocalUser.Value.Id));
}
}
},