2020-05-26 18:12:19 +09:00
// 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 ;
2020-07-22 20:24:55 +09:00
using System.Diagnostics ;
2020-05-26 18:12:19 +09:00
using System.Linq ;
2025-02-25 21:54:13 +09:00
using System.Threading.Tasks ;
2020-06-09 18:53:55 +09:00
using osu.Framework.Allocation ;
using osu.Framework.Graphics ;
using osu.Framework.Graphics.Containers ;
2025-02-26 18:00:16 +09:00
using osu.Framework.Logging ;
2025-02-25 22:37:12 +09:00
using osu.Game.Beatmaps ;
using osu.Game.Database ;
2020-06-09 18:53:55 +09:00
using osu.Game.Graphics.UserInterface ;
2025-02-26 15:04:43 +09:00
using osu.Game.Models ;
2020-05-26 18:12:19 +09:00
using osu.Game.Online.API ;
2025-02-25 22:37:12 +09:00
using osu.Game.Online.API.Requests.Responses ;
2020-12-25 13:38:11 +09:00
using osu.Game.Online.Rooms ;
2021-11-15 16:13:03 +09:00
using osu.Game.Rulesets ;
2020-05-26 18:12:19 +09:00
using osu.Game.Scoring ;
using osu.Game.Screens.Ranking ;
2020-12-25 16:50:00 +01:00
namespace osu.Game.Screens.OnlinePlay.Playlists
2020-05-26 18:12:19 +09:00
{
2024-06-06 10:42:09 +02:00
public abstract partial class PlaylistItemResultsScreen : ResultsScreen
2020-05-26 18:12:19 +09:00
{
2024-06-06 10:42:09 +02:00
protected readonly long RoomId ;
protected readonly PlaylistItem PlaylistItem ;
2020-05-26 18:12:19 +09:00
2024-04-25 09:52:26 +02:00
protected LoadingSpinner LeftSpinner { get ; private set ; } = null ! ;
protected LoadingSpinner CentreSpinner { get ; private set ; } = null ! ;
protected LoadingSpinner RightSpinner { get ; private set ; } = null ! ;
2020-07-31 21:39:50 +09:00
2024-04-25 09:52:26 +02:00
private MultiplayerScores ? higherScores ;
private MultiplayerScores ? lowerScores ;
2020-07-22 19:40:00 +09:00
[Resolved]
2024-06-06 10:42:09 +02:00
protected IAPIProvider API { get ; private set ; } = null ! ;
2020-06-09 18:53:55 +09:00
2021-09-07 15:18:59 +09:00
[Resolved]
2024-06-06 10:42:09 +02:00
protected ScoreManager ScoreManager { get ; private set ; } = null ! ;
2021-09-07 15:18:59 +09:00
2021-11-15 16:13:03 +09:00
[Resolved]
2024-06-06 10:42:09 +02:00
protected RulesetStore Rulesets { get ; private set ; } = null ! ;
2021-11-15 16:13:03 +09:00
2025-02-25 22:37:12 +09:00
[Resolved]
private BeatmapLookupCache beatmapLookupCache { get ; set ; } = null ! ;
2025-02-26 14:49:38 +09:00
[Resolved]
private BeatmapManager beatmapManager { get ; set ; } = null ! ;
2024-06-06 10:42:09 +02:00
protected PlaylistItemResultsScreen ( ScoreInfo ? score , long roomId , PlaylistItem playlistItem )
2024-02-22 19:15:02 +01:00
: base ( score )
2020-05-26 18:12:19 +09:00
{
2024-06-06 10:42:09 +02:00
RoomId = roomId ;
PlaylistItem = playlistItem ;
2020-05-26 18:12:19 +09:00
}
2020-06-09 18:53:55 +09:00
[BackgroundDependencyLoader]
private void load ( )
{
2020-07-31 19:57:05 +09:00
AddInternal ( new Container
2020-06-09 18:53:55 +09:00
{
2020-07-31 19:57:05 +09:00
RelativeSizeAxes = Axes . Both ,
Padding = new MarginPadding { Bottom = TwoLayerButton . SIZE_EXTENDED . Y } ,
Children = new Drawable [ ]
{
2020-07-31 21:39:50 +09:00
LeftSpinner = new PanelListLoadingSpinner ( ScorePanelList )
2020-07-31 19:57:05 +09:00
{
Anchor = Anchor . CentreLeft ,
Origin = Anchor . Centre ,
} ,
2020-07-31 21:39:50 +09:00
CentreSpinner = new PanelListLoadingSpinner ( ScorePanelList )
2020-07-31 19:57:05 +09:00
{
Anchor = Anchor . Centre ,
Origin = Anchor . Centre ,
State = { Value = Score = = null ? Visibility . Visible : Visibility . Hidden } ,
} ,
2020-07-31 21:39:50 +09:00
RightSpinner = new PanelListLoadingSpinner ( ScorePanelList )
2020-07-31 19:57:05 +09:00
{
Anchor = Anchor . CentreRight ,
Origin = Anchor . Centre ,
} ,
}
2020-06-09 18:53:55 +09:00
} ) ;
}
2024-06-06 10:42:09 +02:00
protected abstract APIRequest < MultiplayerScore > CreateScoreRequest ( ) ;
2025-02-25 22:55:55 +09:00
protected override async Task < ScoreInfo [ ] > FetchScores ( )
2020-05-26 18:12:19 +09:00
{
2020-07-22 19:40:00 +09:00
// This performs two requests:
2024-06-06 10:42:09 +02:00
// 1. A request to show the relevant score (and scores around).
2020-07-28 21:40:11 +09:00
// 2. If that fails, a request to index the room starting from the highest score.
2020-07-22 19:40:00 +09:00
2025-02-25 21:54:13 +09:00
var requestTaskSource = new TaskCompletionSource < MultiplayerScore > ( ) ;
2024-06-06 10:42:09 +02:00
var userScoreReq = CreateScoreRequest ( ) ;
2025-02-25 21:54:13 +09:00
userScoreReq . Success + = requestTaskSource . SetResult ;
userScoreReq . Failure + = requestTaskSource . SetException ;
API . Queue ( userScoreReq ) ;
2020-06-09 18:53:55 +09:00
2025-02-25 21:54:13 +09:00
try
2020-06-09 18:53:55 +09:00
{
2025-02-25 22:56:38 +09:00
var userScore = await requestTaskSource . Task . ConfigureAwait ( false ) ;
2020-07-22 19:40:00 +09:00
var allScores = new List < MultiplayerScore > { userScore } ;
2021-12-13 06:54:57 +09:00
// Other scores could have arrived between score submission and entering the results screen. Ensure the local player score position is up to date.
if ( Score ! = null )
{
Score . Position = userScore . Position ;
ScorePanelList . GetPanelForScore ( Score ) . ScorePosition . Value = userScore . Position ;
}
2020-07-22 19:40:00 +09:00
if ( userScore . ScoresAround ? . Higher ! = null )
{
allScores . AddRange ( userScore . ScoresAround . Higher . Scores ) ;
2020-07-28 21:40:11 +09:00
higherScores = userScore . ScoresAround . Higher ;
2020-07-31 22:36:37 +09:00
Debug . Assert ( userScore . Position ! = null ) ;
setPositions ( higherScores , userScore . Position . Value , - 1 ) ;
2020-07-22 19:40:00 +09:00
}
if ( userScore . ScoresAround ? . Lower ! = null )
{
allScores . AddRange ( userScore . ScoresAround . Lower . Scores ) ;
2020-07-28 21:40:11 +09:00
lowerScores = userScore . ScoresAround . Lower ;
2020-07-31 22:36:37 +09:00
Debug . Assert ( userScore . Position ! = null ) ;
setPositions ( lowerScores , userScore . Position . Value , 1 ) ;
2020-07-22 19:40:00 +09:00
}
2025-02-25 22:56:38 +09:00
return await transformScores ( allScores ) . ConfigureAwait ( false ) ;
2025-02-25 21:54:13 +09:00
}
catch
{
2025-02-25 22:56:38 +09:00
return await fetchScoresAround ( ) . ConfigureAwait ( false ) ;
2025-02-25 21:54:13 +09:00
}
2020-07-22 20:24:55 +09:00
}
2025-02-25 22:55:55 +09:00
protected override async Task < ScoreInfo [ ] > FetchNextPage ( int direction )
2020-07-22 20:24:55 +09:00
{
Debug . Assert ( direction = = 1 | | direction = = - 1 ) ;
2024-04-25 09:52:26 +02:00
MultiplayerScores ? pivot = direction = = - 1 ? higherScores : lowerScores ;
2020-07-28 21:40:11 +09:00
if ( pivot ? . Cursor = = null )
2025-02-25 21:54:13 +09:00
return [ ] ;
2020-07-22 20:24:55 +09:00
2025-02-25 21:54:13 +09:00
Schedule ( ( ) = >
{
if ( pivot = = higherScores )
LeftSpinner . Show ( ) ;
else
RightSpinner . Show ( ) ;
} ) ;
2020-07-31 19:57:05 +09:00
2025-02-25 22:56:38 +09:00
return await fetchScoresAround ( pivot ) . ConfigureAwait ( false ) ;
2020-07-31 17:58:48 +09:00
}
/// <summary>
/// Creates a <see cref="IndexPlaylistScoresRequest"/> with an optional score pivot.
/// </summary>
/// <remarks>Does not queue the request.</remarks>
/// <param name="pivot">An optional score pivot to retrieve scores around. Can be null to retrieve scores from the highest score.</param>
2025-02-25 22:55:55 +09:00
private async Task < ScoreInfo [ ] > fetchScoresAround ( MultiplayerScores ? pivot = null )
2020-07-31 17:58:48 +09:00
{
2025-02-25 21:54:13 +09:00
var requestTaskSource = new TaskCompletionSource < IndexedMultiplayerScores > ( ) ;
2020-07-31 17:58:48 +09:00
var indexReq = pivot ! = null
2024-06-06 10:42:09 +02:00
? new IndexPlaylistScoresRequest ( RoomId , PlaylistItem . ID , pivot . Cursor , pivot . Params )
: new IndexPlaylistScoresRequest ( RoomId , PlaylistItem . ID ) ;
2025-02-25 21:54:13 +09:00
indexReq . Success + = requestTaskSource . SetResult ;
indexReq . Failure + = requestTaskSource . SetException ;
API . Queue ( indexReq ) ;
2020-07-22 20:24:55 +09:00
2025-02-25 21:54:13 +09:00
try
2020-07-22 20:24:55 +09:00
{
2025-02-25 22:56:38 +09:00
var index = await requestTaskSource . Task . ConfigureAwait ( false ) ;
2025-02-25 21:54:13 +09:00
2020-07-31 17:58:48 +09:00
if ( pivot = = lowerScores )
2020-07-31 22:36:37 +09:00
{
2025-02-25 21:54:13 +09:00
lowerScores = index ;
setPositions ( index , pivot , 1 ) ;
2020-07-31 22:36:37 +09:00
}
2020-07-31 17:58:48 +09:00
else
2020-07-31 22:36:37 +09:00
{
2025-02-25 21:54:13 +09:00
higherScores = index ;
setPositions ( index , pivot , - 1 ) ;
2020-07-31 22:36:37 +09:00
}
2020-07-22 19:40:00 +09:00
2025-02-25 22:56:38 +09:00
return await transformScores ( index . Scores ) . ConfigureAwait ( false ) ;
2025-02-25 21:54:13 +09:00
}
2025-02-26 18:00:16 +09:00
catch ( Exception ex )
2025-02-25 21:54:13 +09:00
{
2025-02-26 18:00:16 +09:00
Logger . Log ( $"Failed to fetch scores (room: {RoomId}, item: {PlaylistItem.ID}): {ex}" ) ;
2025-02-25 21:54:13 +09:00
return [ ] ;
}
2020-07-22 20:24:55 +09:00
}
2020-07-31 17:58:48 +09:00
/// <summary>
2025-02-25 22:51:36 +09:00
/// Transforms returned <see cref="MultiplayerScores"/> into <see cref="ScoreInfo"/>s.
2020-07-31 17:58:48 +09:00
/// </summary>
/// <param name="scores">The <see cref="MultiplayerScore"/>s that were retrieved from <see cref="APIRequest"/>s.</param>
2025-02-25 22:51:36 +09:00
private async Task < ScoreInfo [ ] > transformScores ( List < MultiplayerScore > scores )
2020-07-22 20:24:55 +09:00
{
2025-02-26 14:49:38 +09:00
int [ ] allBeatmapIds = scores . Select ( s = > s . BeatmapId ) . Distinct ( ) . ToArray ( ) ;
BeatmapInfo [ ] localBeatmaps = allBeatmapIds . Select ( id = > beatmapManager . QueryBeatmap ( b = > b . OnlineID = = id ) )
. Where ( b = > b ! = null )
. ToArray ( ) ! ;
int [ ] missingBeatmapIds = allBeatmapIds . Except ( localBeatmaps . Select ( b = > b . OnlineID ) ) . ToArray ( ) ;
APIBeatmap [ ] onlineBeatmaps = ( await beatmapLookupCache . GetBeatmapsAsync ( missingBeatmapIds ) . ConfigureAwait ( false ) ) . Where ( b = > b ! = null ) . ToArray ( ) ! ;
Dictionary < int , BeatmapInfo > beatmapsById = new Dictionary < int , BeatmapInfo > ( ) ;
2025-02-25 22:37:12 +09:00
2025-02-26 14:49:38 +09:00
foreach ( var beatmap in localBeatmaps )
beatmapsById [ beatmap . OnlineID ] = beatmap ;
foreach ( var beatmap in onlineBeatmaps )
2025-02-25 22:37:12 +09:00
{
2025-02-26 14:49:38 +09:00
// Minimal data required to get various components in this screen to display correctly.
beatmapsById [ beatmap . OnlineID ] = new BeatmapInfo
{
Difficulty = new BeatmapDifficulty ( beatmap . Difficulty ) ,
2025-02-26 15:04:43 +09:00
Metadata =
{
Author = new RealmUser
{
Username = beatmap . Metadata . Author . Username ,
OnlineID = beatmap . Metadata . Author . OnlineID ,
}
} ,
2025-02-26 14:49:38 +09:00
DifficultyName = beatmap . DifficultyName ,
StarRating = beatmap . StarRating ,
Length = beatmap . Length ,
BPM = beatmap . BPM
} ;
}
// Validate that we have all beatmaps we need.
foreach ( int id in allBeatmapIds )
{
if ( ! beatmapsById . ContainsKey ( id ) )
2025-02-26 18:00:16 +09:00
{
Logger . Log ( $"Failed to fetch beatmap {id} to display scores for playlist item {PlaylistItem.ID}" ) ;
beatmapsById [ id ] = Beatmap . Value . BeatmapInfo ;
}
2025-02-26 14:49:38 +09:00
}
2025-02-25 22:37:12 +09:00
2025-02-25 21:54:13 +09:00
// Exclude the score provided to this screen since it's added already.
return scores
. Where ( s = > s . ID ! = Score ? . OnlineID )
2025-02-26 14:49:38 +09:00
. Select ( s = > s . CreateScoreInfo ( ScoreManager , Rulesets , beatmapsById [ s . BeatmapId ] ) )
2025-02-25 21:54:13 +09:00
. OrderByTotalScore ( )
. ToArray ( ) ;
2024-06-06 10:42:09 +02:00
}
2020-07-31 19:57:05 +09:00
2025-02-25 23:17:23 +09:00
protected override void OnScoresAdded ( ScoreInfo [ ] scores )
2020-07-31 19:57:05 +09:00
{
2025-02-25 23:17:23 +09:00
base . OnScoresAdded ( scores ) ;
2020-07-31 19:57:05 +09:00
2025-02-25 23:17:23 +09:00
CentreSpinner . Hide ( ) ;
RightSpinner . Hide ( ) ;
LeftSpinner . Hide ( ) ;
2020-07-31 19:57:05 +09:00
}
2020-07-31 22:36:37 +09:00
/// <summary>
2020-07-31 23:16:55 +09:00
/// Applies positions to all <see cref="MultiplayerScore"/>s referenced to a given pivot.
2020-07-31 22:36:37 +09:00
/// </summary>
/// <param name="scores">The <see cref="MultiplayerScores"/> to set positions on.</param>
/// <param name="pivot">The pivot.</param>
/// <param name="increment">The amount to increment the pivot position by for each <see cref="MultiplayerScore"/> in <paramref name="scores"/>.</param>
2025-02-25 21:54:13 +09:00
private static void setPositions ( MultiplayerScores scores , MultiplayerScores ? pivot , int increment )
2020-07-31 22:36:37 +09:00
= > setPositions ( scores , pivot ? . Scores [ ^ 1 ] . Position ? ? 0 , increment ) ;
/// <summary>
2020-07-31 23:16:55 +09:00
/// Applies positions to all <see cref="MultiplayerScore"/>s referenced to a given pivot.
2020-07-31 22:36:37 +09:00
/// </summary>
/// <param name="scores">The <see cref="MultiplayerScores"/> to set positions on.</param>
/// <param name="pivotPosition">The pivot position.</param>
/// <param name="increment">The amount to increment the pivot position by for each <see cref="MultiplayerScore"/> in <paramref name="scores"/>.</param>
2025-02-25 21:54:13 +09:00
private static void setPositions ( MultiplayerScores scores , int pivotPosition , int increment )
2020-07-31 22:36:37 +09:00
{
foreach ( var s in scores . Scores )
{
pivotPosition + = increment ;
s . Position = pivotPosition ;
}
}
2020-07-31 19:57:05 +09:00
private partial 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 ) ;
}
2020-05-26 18:12:19 +09:00
}
}
}