diff --git a/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeLeaderboard.cs b/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeLeaderboard.cs new file mode 100644 index 0000000000..5fff6bb010 --- /dev/null +++ b/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeLeaderboard.cs @@ -0,0 +1,142 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Utils; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Online.Rooms; +using osu.Game.Overlays; +using osu.Game.Rulesets.Scoring; +using osu.Game.Scoring; +using osu.Game.Screens.OnlinePlay.DailyChallenge; +using osuTK; + +namespace osu.Game.Tests.Visual.DailyChallenge +{ + public partial class TestSceneDailyChallengeLeaderboard : OsuTestScene + { + private DummyAPIAccess dummyAPI => (DummyAPIAccess)API; + + [Cached] + private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Plum); + + [Test] + public void TestBasicBehaviour() + { + DailyChallengeLeaderboard leaderboard = null!; + + AddStep("set up response without user best", () => + { + dummyAPI.HandleRequest = req => + { + if (req is IndexPlaylistScoresRequest indexRequest) + { + indexRequest.TriggerSuccess(createResponse(50, false)); + return true; + } + + return false; + }; + }); + AddStep("create leaderboard", () => Child = leaderboard = new DailyChallengeLeaderboard(new Room { RoomID = { Value = 1 } }, new PlaylistItem(Beatmap.Value.BeatmapInfo)) + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(0.8f), + }); + + AddStep("set up response with user best", () => + { + dummyAPI.HandleRequest = req => + { + if (req is IndexPlaylistScoresRequest indexRequest) + { + indexRequest.TriggerSuccess(createResponse(50, true)); + return true; + } + + return false; + }; + }); + AddStep("force refetch", () => leaderboard.RefetchScores()); + } + + [Test] + public void TestLoadingBehaviour() + { + IndexPlaylistScoresRequest pendingRequest = null!; + DailyChallengeLeaderboard leaderboard = null!; + + AddStep("set up requests handler", () => + { + dummyAPI.HandleRequest = req => + { + if (req is IndexPlaylistScoresRequest indexRequest) + { + pendingRequest = indexRequest; + return true; + } + + return false; + }; + }); + AddStep("create leaderboard", () => Child = leaderboard = new DailyChallengeLeaderboard(new Room { RoomID = { Value = 1 } }, new PlaylistItem(Beatmap.Value.BeatmapInfo)) + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(0.8f), + }); + AddStep("complete load", () => pendingRequest.TriggerSuccess(createResponse(3, true))); + AddStep("force refetch", () => leaderboard.RefetchScores()); + AddStep("complete load", () => pendingRequest.TriggerSuccess(createResponse(4, true))); + } + + private IndexedMultiplayerScores createResponse(int scoreCount, bool returnUserBest) + { + var result = new IndexedMultiplayerScores(); + + for (int i = 0; i < scoreCount; ++i) + { + result.Scores.Add(new MultiplayerScore + { + ID = i, + Accuracy = 1 - (float)i / (2 * scoreCount), + Position = i + 1, + EndedAt = DateTimeOffset.Now, + Passed = true, + Rank = (ScoreRank)RNG.Next((int)ScoreRank.D, (int)ScoreRank.XH), + MaxCombo = 1000 - i, + TotalScore = (long)(1_000_000 * (1 - (float)i / (2 * scoreCount))), + User = new APIUser { Username = $"user {i}" }, + Statistics = new Dictionary() + }); + } + + if (returnUserBest) + { + result.UserScore = new MultiplayerScore + { + ID = 99999, + Accuracy = 0.91, + Position = 4, + EndedAt = DateTimeOffset.Now, + Passed = true, + Rank = ScoreRank.A, + MaxCombo = 100, + TotalScore = 800000, + User = dummyAPI.LocalUser.Value, + Statistics = new Dictionary() + }; + } + + return result; + } + } +} diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs index fca965052f..a52d29a120 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs @@ -413,7 +413,7 @@ namespace osu.Game.Tests.Visual.Playlists }; } - private partial class TestResultsScreen : PlaylistsResultsScreen + private partial class TestResultsScreen : PlaylistItemUserResultsScreen { public new LoadingSpinner LeftSpinner => base.LeftSpinner; public new LoadingSpinner CentreSpinner => base.CentreSpinner; diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneLeaderboardScoreV2.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneLeaderboardScoreV2.cs index 0f5eb06df7..33af4907a1 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneLeaderboardScoreV2.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneLeaderboardScoreV2.cs @@ -50,35 +50,73 @@ namespace osu.Game.Tests.Visual.SongSelect }); } - [SetUp] - public void Setup() => Schedule(() => + [Test] + public void TestSheared() { - Children = new Drawable[] + AddStep("create content", () => { - fillFlow = new FillFlowContainer + Children = new Drawable[] { - Width = relativeWidth, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Spacing = new Vector2(0f, 2f), - Shear = new Vector2(OsuGame.SHEAR, 0) - }, - drawWidthText = new OsuSpriteText(), - }; + fillFlow = new FillFlowContainer + { + Width = relativeWidth, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Spacing = new Vector2(0f, 2f), + Shear = new Vector2(OsuGame.SHEAR, 0) + }, + drawWidthText = new OsuSpriteText(), + }; - foreach (var scoreInfo in getTestScores()) + foreach (var scoreInfo in getTestScores()) + { + fillFlow.Add(new LeaderboardScoreV2(scoreInfo) + { + Rank = scoreInfo.Position, + IsPersonalBest = scoreInfo.User.Id == 2, + Shear = Vector2.Zero, + }); + } + + foreach (var score in fillFlow.Children) + score.Show(); + }); + } + + [Test] + public void TestNonSheared() + { + AddStep("create content", () => { - fillFlow.Add(new LeaderboardScoreV2(scoreInfo, scoreInfo.Position, scoreInfo.User.Id == 2) + Children = new Drawable[] { - Shear = Vector2.Zero, - }); - } + fillFlow = new FillFlowContainer + { + Width = relativeWidth, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Spacing = new Vector2(0f, 2f), + }, + drawWidthText = new OsuSpriteText(), + }; - foreach (var score in fillFlow.Children) - score.Show(); - }); + foreach (var scoreInfo in getTestScores()) + { + fillFlow.Add(new LeaderboardScoreV2(scoreInfo) + { + Rank = scoreInfo.Position, + IsPersonalBest = scoreInfo.User.Id == 2, + }); + } + + foreach (var score in fillFlow.Children) + score.Show(); + }); + } [SetUpSteps] public void SetUpSteps() diff --git a/osu.Game/Online/Rooms/ShowPlaylistScoreRequest.cs b/osu.Game/Online/Rooms/ShowPlaylistScoreRequest.cs new file mode 100644 index 0000000000..d8f977a1d4 --- /dev/null +++ b/osu.Game/Online/Rooms/ShowPlaylistScoreRequest.cs @@ -0,0 +1,23 @@ +// Copyright (c) ppy Pty Ltd . 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.Rooms +{ + public class ShowPlaylistScoreRequest : APIRequest + { + private readonly long roomId; + private readonly long playlistItemId; + private readonly long scoreId; + + public ShowPlaylistScoreRequest(long roomId, long playlistItemId, long scoreId) + { + this.roomId = roomId; + this.playlistItemId = playlistItemId; + this.scoreId = scoreId; + } + + protected override string Target => $@"rooms/{roomId}/playlist/{playlistItemId}/scores/{scoreId}"; + } +} diff --git a/osu.Game/Overlays/Mods/ShearedOverlayContainer.cs b/osu.Game/Overlays/Mods/ShearedOverlayContainer.cs index 1ccf274d0d..9e5a336c17 100644 --- a/osu.Game/Overlays/Mods/ShearedOverlayContainer.cs +++ b/osu.Game/Overlays/Mods/ShearedOverlayContainer.cs @@ -29,6 +29,11 @@ namespace osu.Game.Overlays.Mods /// protected ShearedOverlayHeader Header { get; private set; } = null!; + /// + /// The overlay's footer. + /// + protected Container Footer { get; private set; } = null!; + [Resolved] private ScreenFooter? footer { get; set; } @@ -43,6 +48,11 @@ namespace osu.Game.Overlays.Mods /// protected Container MainAreaContent { get; private set; } = null!; + /// + /// A container for content that is to be displayed inside the footer. + /// + protected Container FooterContent { get; private set; } = null!; + protected override bool StartHidden => true; protected override bool BlockNonPositionalInput => true; @@ -125,7 +135,7 @@ namespace osu.Game.Overlays.Mods if (footer != null) { - footer.SetOverlayContent(this); + footer.SetActiveOverlayContainer(this); if (footer.State.Value == Visibility.Hidden) { @@ -146,7 +156,7 @@ namespace osu.Game.Overlays.Mods if (footer != null) { - footer.ClearOverlayContent(); + footer.ClearActiveOverlayContainer(); if (hideFooterOnPopOut) { diff --git a/osu.Game/Screens/Footer/ScreenFooter.cs b/osu.Game/Screens/Footer/ScreenFooter.cs index dcf64e9291..a841f2a50b 100644 --- a/osu.Game/Screens/Footer/ScreenFooter.cs +++ b/osu.Game/Screens/Footer/ScreenFooter.cs @@ -142,7 +142,7 @@ namespace osu.Game.Screens.Footer temporarilyHiddenButtons.Clear(); overlays.Clear(); - ClearOverlayContent(); + ClearActiveOverlayContainer(); var oldButtons = buttonsFlow.ToArray(); @@ -189,7 +189,7 @@ namespace osu.Game.Screens.Footer private Container? contentContainer; private readonly List temporarilyHiddenButtons = new List(); - public void SetOverlayContent(ShearedOverlayContainer overlay) + public void SetActiveOverlayContainer(ShearedOverlayContainer overlay) { if (contentContainer != null) { @@ -234,7 +234,7 @@ namespace osu.Game.Screens.Footer content.Show(); } - public void ClearOverlayContent() + public void ClearActiveOverlayContainer() { if (contentContainer == null) return; diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs index b8d0dbbe7d..2d58b3b82c 100644 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs @@ -9,7 +9,6 @@ using osu.Framework.Audio; using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.Extensions; -using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; @@ -50,7 +49,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge private readonly Bindable> userMods = new Bindable>(Array.Empty()); private OnlinePlayScreenWaveContainer waves = null!; - private MatchLeaderboard leaderboard = null!; + private DailyChallengeLeaderboard leaderboard = null!; private RoomModSelectOverlay userModsSelectOverlay = null!; private Sample? sampleStart; private IDisposable? userModsSelectOverlayRegistration; @@ -161,7 +160,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge new Box { RelativeSizeAxes = Axes.Both, - Colour = Color4Extensions.FromHex(@"3e3a44") // Temporary. + Colour = colourProvider.Background4, }, new GridContainer { @@ -209,28 +208,17 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge feed = new DailyChallengeEventFeed { RelativeSizeAxes = Axes.Both, + PresentScore = presentScore } ], }, }, null, // Middle column (leaderboard) - new GridContainer + leaderboard = new DailyChallengeLeaderboard(room, playlistItem) { RelativeSizeAxes = Axes.Both, - Content = new[] - { - new Drawable[] - { - new SectionHeader("Leaderboard") - }, - [leaderboard = new MatchLeaderboard { RelativeSizeAxes = Axes.Both }], - }, - RowDimensions = new[] - { - new Dimension(GridSizeMode.AutoSize), - new Dimension(), - } + PresentScore = presentScore, }, // Spacer null, @@ -272,7 +260,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge new Box { RelativeSizeAxes = Axes.Both, - Colour = Color4Extensions.FromHex(@"28242d") // Temporary. + Colour = colourProvider.Background5, }, footerButtons = new FillFlowContainer { @@ -326,6 +314,12 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge metadataClient.MultiplayerRoomScoreSet += onRoomScoreSet; } + private void presentScore(long id) + { + if (this.IsCurrentScreen()) + this.Push(new PlaylistItemScoreResultsScreen(room.RoomID.Value!.Value, playlistItem, id)); + } + private void onRoomScoreSet(MultiplayerRoomScoreSetEvent e) { if (e.RoomID != room.RoomID.Value || e.PlaylistItemID != playlistItem.ID) @@ -347,6 +341,9 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge { breakdown.AddNewScore(ev); feed.AddNewScore(ev); + + if (e.NewRank <= 50) + Schedule(() => leaderboard.RefetchScores()); }); }); } diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeEventFeed.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeEventFeed.cs index c38a921e43..e76238abad 100644 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeEventFeed.cs +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeEventFeed.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . 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.Framework.Allocation; @@ -19,6 +20,8 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge { private DailyChallengeEventFeedFlow flow = null!; + public Action? PresentScore { get; init; } + [BackgroundDependencyLoader] private void load() { @@ -48,6 +51,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge { Anchor = Anchor.BottomCentre, Origin = Anchor.BottomCentre, + PresentScore = PresentScore, }; flow.Add(row); row.Delay(15000).Then().FadeOut(300, Easing.OutQuint).Expire(); @@ -78,6 +82,8 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge { private readonly NewScoreEvent newScore; + public Action? PresentScore { get; init; } + public NewScoreEventRow(NewScoreEvent newScore) { this.newScore = newScore; @@ -115,7 +121,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge text.AddUserLink(newScore.User); text.AddText(" got "); - text.AddLink($"{newScore.TotalScore:N0} points", () => { }); // TODO: present the score here + text.AddLink($"{newScore.TotalScore:N0} points", () => PresentScore?.Invoke(newScore.ScoreID)); if (newScore.NewRank != null) text.AddText($" and achieved rank #{newScore.NewRank.Value:N0}"); diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeLeaderboard.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeLeaderboard.cs new file mode 100644 index 0000000000..4d4ae755fc --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeLeaderboard.cs @@ -0,0 +1,175 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Linq; +using System.Threading; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Beatmaps; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.UserInterface; +using osu.Game.Online.API; +using osu.Game.Online.Rooms; +using osu.Game.Rulesets; +using osu.Game.Scoring; +using osu.Game.Screens.SelectV2.Leaderboards; +using osuTK; + +namespace osu.Game.Screens.OnlinePlay.DailyChallenge +{ + public partial class DailyChallengeLeaderboard : CompositeDrawable + { + public Action? PresentScore { get; init; } + + private readonly Room room; + private readonly PlaylistItem playlistItem; + + private FillFlowContainer scoreFlow = null!; + private Container userBestContainer = null!; + private SectionHeader userBestHeader = null!; + private LoadingLayer loadingLayer = null!; + + private CancellationTokenSource? cancellationTokenSource; + + [Resolved] + private IAPIProvider api { get; set; } = null!; + + [Resolved] + private ScoreManager scoreManager { get; set; } = null!; + + [Resolved] + private RulesetStore rulesets { get; set; } = null!; + + [Resolved] + private IBindable beatmap { get; set; } = null!; + + public DailyChallengeLeaderboard(Room room, PlaylistItem playlistItem) + { + this.room = room; + this.playlistItem = playlistItem; + } + + [BackgroundDependencyLoader] + private void load() + { + InternalChild = new GridContainer + { + RelativeSizeAxes = Axes.Both, + RowDimensions = + [ + new Dimension(GridSizeMode.AutoSize), + new Dimension(), + new Dimension(GridSizeMode.AutoSize), + new Dimension(GridSizeMode.AutoSize) + ], + Content = new[] + { + new Drawable[] { new SectionHeader("Leaderboard") }, + new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + new OsuScrollContainer + { + RelativeSizeAxes = Axes.Both, + Child = scoreFlow = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding { Right = 20, }, + Direction = FillDirection.Vertical, + Spacing = new Vector2(5), + Scale = new Vector2(0.8f), + Width = 1 / 0.8f, + } + }, + loadingLayer = new LoadingLayer + { + RelativeSizeAxes = Axes.Both, + }, + } + } + }, + new Drawable[] { userBestHeader = new SectionHeader("Personal best") { Alpha = 0, } }, + new Drawable[] + { + userBestContainer = new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding { Right = 20, }, + Scale = new Vector2(0.8f), + Width = 1 / 0.8f, + } + } + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + RefetchScores(); + } + + public void RefetchScores() + { + var request = new IndexPlaylistScoresRequest(room.RoomID.Value!.Value, playlistItem.ID); + + request.Success += req => + { + var best = req.Scores.Select(s => s.CreateScoreInfo(scoreManager, rulesets, playlistItem, beatmap.Value.BeatmapInfo)).ToArray(); + var userBest = req.UserScore?.CreateScoreInfo(scoreManager, rulesets, playlistItem, beatmap.Value.BeatmapInfo); + + cancellationTokenSource?.Cancel(); + cancellationTokenSource = null; + cancellationTokenSource ??= new CancellationTokenSource(); + + if (best.Length == 0) + { + scoreFlow.Clear(); + loadingLayer.Hide(); + } + else + { + LoadComponentsAsync(best.Select(s => new LeaderboardScoreV2(s, sheared: false) + { + Rank = s.Position, + IsPersonalBest = s.UserID == api.LocalUser.Value.Id, + Action = () => PresentScore?.Invoke(s.OnlineID), + }), loaded => + { + scoreFlow.Clear(); + scoreFlow.AddRange(loaded); + scoreFlow.FadeTo(1, 400, Easing.OutQuint); + loadingLayer.Hide(); + }, cancellationTokenSource.Token); + } + + userBestContainer.Clear(); + + if (userBest != null) + { + userBestContainer.Add(new LeaderboardScoreV2(userBest, sheared: false) + { + Rank = userBest.Position, + IsPersonalBest = true, + Action = () => PresentScore?.Invoke(userBest.OnlineID), + }); + } + + userBestHeader.FadeTo(userBest == null ? 0 : 1); + }; + + loadingLayer.Show(); + scoreFlow.FadeTo(0.5f, 400, Easing.OutQuint); + api.Queue(request); + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index 78ab8cfa6c..7c8931c04e 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -80,6 +80,9 @@ namespace osu.Game.Screens.OnlinePlay.Match [Resolved(canBeNull: true)] protected OnlinePlayScreen ParentScreen { get; private set; } + [Resolved] + private PreviewTrackManager previewTrackManager { get; set; } = null!; + [Cached] private readonly OnlinePlayBeatmapAvailabilityTracker beatmapAvailabilityTracker = new OnlinePlayBeatmapAvailabilityTracker(); @@ -483,6 +486,8 @@ namespace osu.Game.Screens.OnlinePlay.Match { UserModsSelectOverlay.Hide(); endHandlingTrack(); + + previewTrackManager.StopAnyPlaying(this); } private void endHandlingTrack() diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerResultsScreen.cs index 6ed75508dc..c439df82a6 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerResultsScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerResultsScreen.cs @@ -7,7 +7,7 @@ using osu.Game.Screens.OnlinePlay.Playlists; namespace osu.Game.Screens.OnlinePlay.Multiplayer { - public partial class MultiplayerResultsScreen : PlaylistsResultsScreen + public partial class MultiplayerResultsScreen : PlaylistItemUserResultsScreen { public MultiplayerResultsScreen(ScoreInfo score, long roomId, PlaylistItem playlistItem) : base(score, roomId, playlistItem) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs similarity index 80% rename from osu.Game/Screens/OnlinePlay/Playlists/PlaylistsResultsScreen.cs rename to osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs index fdb83b5ae8..dc06b88823 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsResultsScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs @@ -17,10 +17,10 @@ using osu.Game.Screens.Ranking; namespace osu.Game.Screens.OnlinePlay.Playlists { - public partial class PlaylistsResultsScreen : ResultsScreen + public abstract partial class PlaylistItemResultsScreen : ResultsScreen { - private readonly long roomId; - private readonly PlaylistItem playlistItem; + protected readonly long RoomId; + protected readonly PlaylistItem PlaylistItem; protected LoadingSpinner LeftSpinner { get; private set; } = null!; protected LoadingSpinner CentreSpinner { get; private set; } = null!; @@ -30,19 +30,19 @@ namespace osu.Game.Screens.OnlinePlay.Playlists private MultiplayerScores? lowerScores; [Resolved] - private IAPIProvider api { get; set; } = null!; + protected IAPIProvider API { get; private set; } = null!; [Resolved] - private ScoreManager scoreManager { get; set; } = null!; + protected ScoreManager ScoreManager { get; private set; } = null!; [Resolved] - private RulesetStore rulesets { get; set; } = null!; + protected RulesetStore Rulesets { get; private set; } = null!; - public PlaylistsResultsScreen(ScoreInfo? score, long roomId, PlaylistItem playlistItem) + protected PlaylistItemResultsScreen(ScoreInfo? score, long roomId, PlaylistItem playlistItem) : base(score) { - this.roomId = roomId; - this.playlistItem = playlistItem; + RoomId = roomId; + PlaylistItem = playlistItem; } [BackgroundDependencyLoader] @@ -74,13 +74,15 @@ namespace osu.Game.Screens.OnlinePlay.Playlists }); } - protected override APIRequest FetchScores(Action> scoresCallback) + protected abstract APIRequest CreateScoreRequest(); + + protected sealed override APIRequest FetchScores(Action> scoresCallback) { // This performs two requests: - // 1. A request to show the user's score (and scores around). + // 1. A request to show the relevant score (and scores around). // 2. If that fails, a request to index the room starting from the highest score. - var userScoreReq = new ShowPlaylistUserScoreRequest(roomId, playlistItem.ID, api.LocalUser.Value.Id); + var userScoreReq = CreateScoreRequest(); userScoreReq.Success += userScore => { @@ -111,11 +113,15 @@ namespace osu.Game.Screens.OnlinePlay.Playlists setPositions(lowerScores, userScore.Position.Value, 1); } - performSuccessCallback(scoresCallback, allScores); + Schedule(() => + { + PerformSuccessCallback(scoresCallback, allScores); + hideLoadingSpinners(); + }); }; // On failure, fallback to a normal index. - userScoreReq.Failure += _ => api.Queue(createIndexRequest(scoresCallback)); + userScoreReq.Failure += _ => API.Queue(createIndexRequest(scoresCallback)); return userScoreReq; } @@ -147,8 +153,8 @@ namespace osu.Game.Screens.OnlinePlay.Playlists private APIRequest createIndexRequest(Action> scoresCallback, MultiplayerScores? pivot = null) { var indexReq = pivot != null - ? new IndexPlaylistScoresRequest(roomId, playlistItem.ID, pivot.Cursor, pivot.Params) - : new IndexPlaylistScoresRequest(roomId, playlistItem.ID); + ? new IndexPlaylistScoresRequest(RoomId, PlaylistItem.ID, pivot.Cursor, pivot.Params) + : new IndexPlaylistScoresRequest(RoomId, PlaylistItem.ID); indexReq.Success += r => { @@ -163,7 +169,11 @@ namespace osu.Game.Screens.OnlinePlay.Playlists setPositions(r, pivot, -1); } - performSuccessCallback(scoresCallback, r.Scores, r); + Schedule(() => + { + PerformSuccessCallback(scoresCallback, r.Scores, r); + hideLoadingSpinners(r); + }); }; indexReq.Failure += _ => hideLoadingSpinners(pivot); @@ -177,26 +187,15 @@ namespace osu.Game.Screens.OnlinePlay.Playlists /// The callback to invoke with the final s. /// The s that were retrieved from s. /// An optional pivot around which the scores were retrieved. - private void performSuccessCallback(Action> callback, List scores, MultiplayerScores? pivot = null) => Schedule(() => + protected virtual ScoreInfo[] PerformSuccessCallback(Action> callback, List scores, MultiplayerScores? pivot = null) { - var scoreInfos = scores.Select(s => s.CreateScoreInfo(scoreManager, rulesets, playlistItem, Beatmap.Value.BeatmapInfo)).OrderByTotalScore().ToArray(); + 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. + callback.Invoke(scoreInfos); - // 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)); - - hideLoadingSpinners(pivot); - }); + return scoreInfos; + } private void hideLoadingSpinners(MultiplayerScores? pivot = null) { diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemScoreResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemScoreResultsScreen.cs new file mode 100644 index 0000000000..831b6538a7 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemScoreResultsScreen.cs @@ -0,0 +1,37 @@ +// Copyright (c) ppy Pty Ltd . 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 +{ + /// + /// Shows a selected arbitrary score for a playlist item, with scores around included. + /// + public partial class PlaylistItemScoreResultsScreen : PlaylistItemResultsScreen + { + private readonly long scoreId; + + public PlaylistItemScoreResultsScreen(long roomId, PlaylistItem playlistItem, long scoreId) + : base(null, roomId, playlistItem) + { + this.scoreId = scoreId; + } + + protected override APIRequest CreateScoreRequest() => new ShowPlaylistScoreRequest(RoomId, PlaylistItem.ID, scoreId); + + protected override ScoreInfo[] PerformSuccessCallback(Action> callback, List scores, MultiplayerScores? pivot = null) + { + var scoreInfos = base.PerformSuccessCallback(callback, scores, pivot); + + Schedule(() => SelectedScore.Value = scoreInfos.SingleOrDefault(score => score.OnlineID == scoreId)); + + return scoreInfos; + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemUserResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemUserResultsScreen.cs new file mode 100644 index 0000000000..e038cf3288 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemUserResultsScreen.cs @@ -0,0 +1,46 @@ +// Copyright (c) ppy Pty Ltd . 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 +{ + /// + /// Shows the user's best score for a given playlist item, with scores around included. + /// + public partial class PlaylistItemUserResultsScreen : PlaylistItemResultsScreen + { + public PlaylistItemUserResultsScreen(ScoreInfo? score, long roomId, PlaylistItem playlistItem) + : base(score, roomId, playlistItem) + { + } + + protected override APIRequest CreateScoreRequest() => new ShowPlaylistUserScoreRequest(RoomId, PlaylistItem.ID, API.LocalUser.Value.Id); + + protected override ScoreInfo[] PerformSuccessCallback(Action> callback, List 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; + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs index 48f63731e1..4a2d8f8f6b 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs @@ -58,7 +58,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists protected override ResultsScreen CreateResults(ScoreInfo score) { Debug.Assert(Room.RoomID.Value != null); - return new PlaylistsResultsScreen(score, Room.RoomID.Value.Value, PlaylistItem) + return new PlaylistItemUserResultsScreen(score, Room.RoomID.Value.Value, PlaylistItem) { AllowRetry = true, ShowUserStatistics = true, diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs index 3fb9de428a..3126bbf2eb 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs @@ -114,7 +114,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists RequestResults = item => { Debug.Assert(RoomId.Value != null); - ParentScreen?.Push(new PlaylistsResultsScreen(null, RoomId.Value.Value, item)); + ParentScreen?.Push(new PlaylistItemUserResultsScreen(null, RoomId.Value.Value, item)); } } }, diff --git a/osu.Game/Screens/SelectV2/Leaderboards/LeaderboardScoreV2.cs b/osu.Game/Screens/SelectV2/Leaderboards/LeaderboardScoreV2.cs index 804a9d24b7..700f889d7f 100644 --- a/osu.Game/Screens/SelectV2/Leaderboards/LeaderboardScoreV2.cs +++ b/osu.Game/Screens/SelectV2/Leaderboards/LeaderboardScoreV2.cs @@ -43,6 +43,9 @@ namespace osu.Game.Screens.SelectV2.Leaderboards { public partial class LeaderboardScoreV2 : OsuClickableContainer, IHasContextMenu, IHasCustomTooltip { + public int? Rank { get; init; } + public bool IsPersonalBest { get; init; } + private const float expanded_right_content_width = 210; private const float grade_width = 40; private const float username_min_width = 125; @@ -52,15 +55,12 @@ namespace osu.Game.Screens.SelectV2.Leaderboards private const float rank_label_visibility_width_cutoff = rank_label_width + height + username_min_width + statistics_regular_min_width + expanded_right_content_width; private readonly ScoreInfo score; + private readonly bool sheared; private const int height = 60; private const int corner_radius = 10; private const int transition_duration = 200; - private readonly int? rank; - - private readonly bool isPersonalBest; - private Colour4 foregroundColour; private Colour4 backgroundColour; private ColourInfo totalScoreBackgroundGradient; @@ -104,13 +104,12 @@ namespace osu.Game.Screens.SelectV2.Leaderboards public ITooltip GetCustomTooltip() => new LeaderboardScoreTooltip(); public virtual ScoreInfo TooltipContent => score; - public LeaderboardScoreV2(ScoreInfo score, int? rank, bool isPersonalBest = false) + public LeaderboardScoreV2(ScoreInfo score, bool sheared = true) { this.score = score; - this.rank = rank; - this.isPersonalBest = isPersonalBest; + this.sheared = sheared; - Shear = new Vector2(OsuGame.SHEAR, 0); + Shear = new Vector2(sheared ? OsuGame.SHEAR : 0, 0); RelativeSizeAxes = Axes.X; Height = height; } @@ -120,8 +119,8 @@ namespace osu.Game.Screens.SelectV2.Leaderboards { var user = score.User; - foregroundColour = isPersonalBest ? colourProvider.Background1 : colourProvider.Background5; - backgroundColour = isPersonalBest ? colourProvider.Background2 : colourProvider.Background4; + foregroundColour = IsPersonalBest ? colourProvider.Background1 : colourProvider.Background5; + backgroundColour = IsPersonalBest ? colourProvider.Background2 : colourProvider.Background4; totalScoreBackgroundGradient = ColourInfo.GradientHorizontal(backgroundColour.Opacity(0), backgroundColour); statisticsLabels = GetStatistics(score).Select(s => new ScoreComponentLabel(s, score) @@ -159,7 +158,7 @@ namespace osu.Game.Screens.SelectV2.Leaderboards { AutoSizeAxes = Axes.X, RelativeSizeAxes = Axes.Y, - Child = rankLabel = new RankLabel(rank) + Child = rankLabel = new RankLabel(Rank, sheared) { Width = rank_label_width, RelativeSizeAxes = Axes.Y, @@ -243,7 +242,7 @@ namespace osu.Game.Screens.SelectV2.Leaderboards { RelativeSizeAxes = Axes.Both, User = score.User, - Shear = new Vector2(-OsuGame.SHEAR, 0), + Shear = new Vector2(sheared ? -OsuGame.SHEAR : 0, 0), Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, Colour = ColourInfo.GradientHorizontal(Colour4.White.Opacity(0.5f), Colour4.FromHex(@"222A27").Opacity(1)), @@ -274,7 +273,7 @@ namespace osu.Game.Screens.SelectV2.Leaderboards Anchor = Anchor.Centre, Origin = Anchor.Centre, Scale = new Vector2(1.1f), - Shear = new Vector2(-OsuGame.SHEAR, 0), + Shear = new Vector2(sheared ? -OsuGame.SHEAR : 0, 0), RelativeSizeAxes = Axes.Both, }) { @@ -292,7 +291,7 @@ namespace osu.Game.Screens.SelectV2.Leaderboards RelativeSizeAxes = Axes.Both, Colour = Colour4.Black.Opacity(0.5f), }, - new RankLabel(rank) + new RankLabel(Rank, sheared) { AutoSizeAxes = Axes.Both, Anchor = Anchor.Centre, @@ -314,7 +313,7 @@ namespace osu.Game.Screens.SelectV2.Leaderboards { flagBadgeAndDateContainer = new FillFlowContainer { - Shear = new Vector2(-OsuGame.SHEAR, 0), + Shear = new Vector2(sheared ? -OsuGame.SHEAR : 0, 0), Direction = FillDirection.Horizontal, Spacing = new Vector2(5), AutoSizeAxes = Axes.Both, @@ -338,7 +337,7 @@ namespace osu.Game.Screens.SelectV2.Leaderboards nameLabel = new TruncatingSpriteText { RelativeSizeAxes = Axes.X, - Shear = new Vector2(-OsuGame.SHEAR, 0), + Shear = new Vector2(sheared ? -OsuGame.SHEAR : 0, 0), Text = user.Username, Font = OsuFont.GetFont(size: 20, weight: FontWeight.SemiBold) } @@ -354,7 +353,7 @@ namespace osu.Game.Screens.SelectV2.Leaderboards Name = @"Statistics container", Padding = new MarginPadding { Right = 40 }, Spacing = new Vector2(25, 0), - Shear = new Vector2(-OsuGame.SHEAR, 0), + Shear = new Vector2(sheared ? -OsuGame.SHEAR : 0, 0), Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, AutoSizeAxes = Axes.Both, @@ -412,7 +411,7 @@ namespace osu.Game.Screens.SelectV2.Leaderboards }, RankContainer = new Container { - Shear = new Vector2(-OsuGame.SHEAR, 0), + Shear = new Vector2(sheared ? -OsuGame.SHEAR : 0, 0), Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, RelativeSizeAxes = Axes.Y, @@ -470,7 +469,7 @@ namespace osu.Game.Screens.SelectV2.Leaderboards Anchor = Anchor.TopRight, Origin = Anchor.TopRight, UseFullGlyphHeight = false, - Shear = new Vector2(-OsuGame.SHEAR, 0), + Shear = new Vector2(sheared ? -OsuGame.SHEAR : 0, 0), Current = scoreManager.GetBindableTotalScoreString(score), Font = OsuFont.GetFont(size: 30, weight: FontWeight.Light), }, @@ -478,7 +477,7 @@ namespace osu.Game.Screens.SelectV2.Leaderboards { Anchor = Anchor.TopRight, Origin = Anchor.TopRight, - Shear = new Vector2(-OsuGame.SHEAR, 0), + Shear = new Vector2(sheared ? -OsuGame.SHEAR : 0, 0), AutoSizeAxes = Axes.Both, Direction = FillDirection.Horizontal, Spacing = new Vector2(2f, 0f), @@ -656,14 +655,14 @@ namespace osu.Game.Screens.SelectV2.Leaderboards private partial class RankLabel : Container, IHasTooltip { - public RankLabel(int? rank) + public RankLabel(int? rank, bool sheared) { if (rank >= 1000) TooltipText = $"#{rank:N0}"; Child = new OsuSpriteText { - Shear = new Vector2(-OsuGame.SHEAR, 0), + Shear = new Vector2(sheared ? -OsuGame.SHEAR : 0, 0), Anchor = Anchor.Centre, Origin = Anchor.Centre, Font = OsuFont.GetFont(size: 20, weight: FontWeight.SemiBold, italics: true), diff --git a/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs b/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs index ef4539ba56..36e256b920 100644 --- a/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs +++ b/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs @@ -99,6 +99,54 @@ namespace osu.Game.Tests.Visual.OnlinePlay }); return true; + case IndexPlaylistScoresRequest roomLeaderboardRequest: + roomLeaderboardRequest.TriggerSuccess(new IndexedMultiplayerScores + { + Scores = + { + new MultiplayerScore + { + ID = currentScoreId++, + Accuracy = 1, + Position = 1, + EndedAt = DateTimeOffset.Now, + Passed = true, + Rank = ScoreRank.S, + MaxCombo = 1000, + TotalScore = 1000000, + User = new APIUser { Username = "best user" }, + Statistics = new Dictionary() + }, + new MultiplayerScore + { + ID = currentScoreId++, + Accuracy = 0.7, + Position = 2, + EndedAt = DateTimeOffset.Now, + Passed = true, + Rank = ScoreRank.B, + MaxCombo = 100, + TotalScore = 200000, + User = new APIUser { Username = "worst user" }, + Statistics = new Dictionary() + }, + }, + UserScore = new MultiplayerScore + { + ID = currentScoreId++, + Accuracy = 0.91, + Position = 4, + EndedAt = DateTimeOffset.Now, + Passed = true, + Rank = ScoreRank.A, + MaxCombo = 100, + TotalScore = 800000, + User = localUser, + Statistics = new Dictionary() + }, + }); + return true; + case PartRoomRequest partRoomRequest: partRoomRequest.TriggerSuccess(); return true;