2020-03-17 16:43:16 +08: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.
2022-06-17 15:37:17 +08:00
#nullable disable
2020-03-18 17:28:42 +08:00
using System ;
2020-05-26 16:00:41 +08:00
using System.Collections.Generic ;
2020-06-19 16:28:35 +08:00
using System.Linq ;
2024-01-29 15:13:30 +08:00
using JetBrains.Annotations ;
2020-03-17 16:43:16 +08:00
using osu.Framework.Allocation ;
2022-07-22 18:06:31 +08:00
using osu.Framework.Audio ;
using osu.Framework.Audio.Sample ;
2020-05-28 20:40:01 +08:00
using osu.Framework.Bindables ;
2020-03-17 16:43:16 +08:00
using osu.Framework.Extensions.Color4Extensions ;
using osu.Framework.Graphics ;
using osu.Framework.Graphics.Containers ;
using osu.Framework.Graphics.Shapes ;
2020-10-11 09:30:13 +08:00
using osu.Framework.Input.Bindings ;
2021-09-16 17:26:12 +08:00
using osu.Framework.Input.Events ;
2020-03-17 16:43:16 +08:00
using osu.Framework.Screens ;
2021-06-09 16:18:52 +08:00
using osu.Game.Graphics ;
2020-03-17 21:21:16 +08:00
using osu.Game.Graphics.Containers ;
2020-03-17 16:43:16 +08:00
using osu.Game.Graphics.UserInterface ;
2020-10-11 09:30:13 +08:00
using osu.Game.Input.Bindings ;
2024-01-04 11:15:48 +08:00
using osu.Game.Localisation ;
2020-05-16 18:00:20 +08:00
using osu.Game.Online.API ;
2024-01-04 11:15:48 +08:00
using osu.Game.Online.Placeholders ;
2024-02-20 22:52:15 +08:00
using osu.Game.Overlays ;
2020-03-17 16:43:16 +08:00
using osu.Game.Scoring ;
2020-03-17 16:45:25 +08:00
using osu.Game.Screens.Play ;
2024-02-20 22:52:15 +08:00
using osu.Game.Screens.Ranking.Expanded.Accuracy ;
2020-06-16 16:49:43 +08:00
using osu.Game.Screens.Ranking.Statistics ;
2020-03-17 16:43:16 +08:00
using osuTK ;
namespace osu.Game.Screens.Ranking
{
2021-01-04 17:24:21 +08:00
public abstract partial class ResultsScreen : ScreenWithBeatmapBackground , IKeyBindingHandler < GlobalAction >
2020-03-17 16:43:16 +08:00
{
protected const float BACKGROUND_BLUR = 20 ;
2020-06-17 18:28:40 +08:00
private static readonly float screen_height = 768 - TwoLayerButton . SIZE_EXTENDED . Y ;
2020-03-17 16:43:16 +08:00
public override bool DisallowExternalBeatmapRulesetChanges = > true ;
2023-07-25 19:00:31 +08:00
public override bool? AllowGlobalTrackControl = > true ;
2024-02-20 22:52:15 +08:00
protected override OverlayActivation InitialOverlayActivationMode = > OverlayActivation . UserTriggered ;
2020-05-28 20:40:01 +08:00
public readonly Bindable < ScoreInfo > SelectedScore = new Bindable < ScoreInfo > ( ) ;
2024-01-29 15:13:30 +08:00
[CanBeNull]
2020-05-28 20:40:01 +08:00
public readonly ScoreInfo Score ;
2020-07-31 18:57:05 +08:00
protected ScorePanelList ScorePanelList { get ; private set ; }
2020-05-28 20:40:01 +08:00
2021-08-13 15:14:23 +08:00
protected VerticalScrollContainer VerticalScrollContent { get ; private set ; }
2020-03-17 16:45:25 +08:00
[Resolved(CanBeNull = true)]
private Player player { get ; set ; }
2020-05-16 18:00:20 +08:00
[Resolved]
private IAPIProvider api { get ; set ; }
2023-08-02 00:09:48 +08:00
protected StatisticsPanel StatisticsPanel { get ; private set ; }
2023-08-01 18:00:01 +08:00
2020-03-17 16:43:16 +08:00
private Drawable bottomPanel ;
2020-06-19 16:28:35 +08:00
private Container < ScorePanel > detachedPanelContainer ;
2020-03-17 16:43:16 +08:00
2021-09-07 14:18:59 +08:00
private bool lastFetchCompleted ;
2020-07-28 19:58:13 +08:00
2024-02-23 02:15:02 +08:00
/// <summary>
/// Whether the user can retry the beatmap from the results screen.
/// </summary>
public bool AllowRetry { get ; init ; }
/// <summary>
/// Whether the user can watch the replay of the completed play from the results screen.
/// </summary>
public bool AllowWatchingReplay { get ; init ; } = true ;
2020-07-31 18:57:05 +08:00
2024-02-23 02:49:14 +08:00
/// <summary>
/// Whether the user's personal statistics should be shown on the extended statistics panel
/// after clicking the score panel associated with the <see cref="ResultsScreen.Score"/> being presented.
/// Requires <see cref="Score"/> to be present.
/// </summary>
public bool ShowUserStatistics { get ; init ; }
2022-07-22 18:06:31 +08:00
private Sample popInSample ;
2024-02-23 02:15:02 +08:00
protected ResultsScreen ( [ CanBeNull ] ScoreInfo score )
2020-03-17 16:43:16 +08:00
{
2020-03-29 22:50:16 +08:00
Score = score ;
2020-05-28 20:40:01 +08:00
SelectedScore . Value = score ;
2020-03-17 16:43:16 +08:00
}
[BackgroundDependencyLoader]
2022-07-22 18:06:31 +08:00
private void load ( AudioManager audio )
2020-03-17 16:43:16 +08:00
{
2020-03-29 22:52:50 +08:00
FillFlowContainer buttons ;
2022-07-22 18:06:31 +08:00
popInSample = audio . Samples . Get ( @"UI/overlay-pop-in" ) ;
2020-05-22 19:39:02 +08:00
InternalChild = new GridContainer
2020-03-17 16:43:16 +08:00
{
2020-05-22 19:39:02 +08:00
RelativeSizeAxes = Axes . Both ,
Content = new [ ]
2020-03-17 16:43:16 +08:00
{
2020-05-22 19:39:02 +08:00
new Drawable [ ]
2020-03-17 21:21:16 +08:00
{
2021-08-13 15:14:23 +08:00
VerticalScrollContent = new VerticalScrollContainer
2020-03-17 16:43:16 +08:00
{
2020-06-17 21:29:00 +08:00
RelativeSizeAxes = Axes . Both ,
2020-06-30 15:36:53 +08:00
ScrollbarVisible = false ,
Child = new Container
2020-05-22 19:39:02 +08:00
{
2020-06-30 15:36:53 +08:00
RelativeSizeAxes = Axes . Both ,
Children = new Drawable [ ]
2020-06-17 18:28:40 +08:00
{
2024-02-23 02:49:14 +08:00
StatisticsPanel = createStatisticsPanel ( ) . With ( panel = >
2020-07-02 05:31:06 +08:00
{
2022-12-24 17:35:42 +08:00
panel . RelativeSizeAxes = Axes . Both ;
panel . Score . BindTarget = SelectedScore ;
} ) ,
2020-07-31 18:57:05 +08:00
ScorePanelList = new ScorePanelList
2020-06-17 18:28:40 +08:00
{
2020-06-30 15:36:53 +08:00
RelativeSizeAxes = Axes . Both ,
SelectedScore = { BindTarget = SelectedScore } ,
2023-08-01 18:00:01 +08:00
PostExpandAction = ( ) = > StatisticsPanel . ToggleVisibility ( )
2020-06-30 15:36:53 +08:00
} ,
detachedPanelContainer = new Container < ScorePanel >
{
RelativeSizeAxes = Axes . Both
} ,
}
2020-05-22 19:39:02 +08:00
}
2020-06-30 15:36:53 +08:00
} ,
2020-05-22 19:39:02 +08:00
} ,
new [ ]
{
bottomPanel = new Container
2020-03-17 16:43:16 +08:00
{
2020-05-22 19:39:02 +08:00
Anchor = Anchor . BottomLeft ,
Origin = Anchor . BottomLeft ,
RelativeSizeAxes = Axes . X ,
Height = TwoLayerButton . SIZE_EXTENDED . Y ,
Alpha = 0 ,
2020-03-17 16:43:16 +08:00
Children = new Drawable [ ]
{
2020-05-22 19:39:02 +08:00
new Box
{
RelativeSizeAxes = Axes . Both ,
Colour = Color4Extensions . FromHex ( "#333" )
} ,
buttons = new FillFlowContainer
{
Anchor = Anchor . Centre ,
Origin = Anchor . Centre ,
AutoSizeAxes = Axes . Both ,
Spacing = new Vector2 ( 5 ) ,
2020-12-20 23:14:54 +08:00
Direction = FillDirection . Horizontal
2020-05-22 19:39:02 +08:00
}
2020-03-17 16:43:16 +08:00
}
}
}
2020-05-22 19:39:02 +08:00
} ,
RowDimensions = new [ ]
{
new Dimension ( ) ,
new Dimension ( GridSizeMode . AutoSize )
2020-03-17 16:43:16 +08:00
}
} ;
2020-03-17 16:45:25 +08:00
2020-05-28 20:40:01 +08:00
if ( Score ! = null )
2020-11-20 13:32:23 +08:00
{
// only show flair / animation when arriving after watching a play that isn't autoplay.
2023-08-30 05:28:50 +08:00
bool shouldFlair = player ! = null & & ! Score . User . IsBot ;
2020-11-20 13:32:23 +08:00
ScorePanelList . AddScore ( Score , shouldFlair ) ;
2024-02-20 22:52:15 +08:00
// this is mostly for medal display.
// we don't want the medal animation to trample on the results screen animation, so we (ab)use `OverlayActivationMode`
// to give the results screen enough time to play the animation out before the medals can be shown.
Scheduler . AddDelayed ( ( ) = > OverlayActivationMode . Value = OverlayActivation . All , shouldFlair ? AccuracyCircle . TOTAL_DURATION + 1000 : 0 ) ;
2020-11-20 13:32:23 +08:00
}
2020-05-28 20:40:01 +08:00
2024-02-23 02:15:02 +08:00
if ( AllowWatchingReplay )
2020-12-20 23:14:54 +08:00
{
2023-06-21 16:50:06 +08:00
buttons . Add ( new ReplayDownloadButton ( SelectedScore . Value )
2020-12-20 23:14:54 +08:00
{
Score = { BindTarget = SelectedScore } ,
Width = 300
} ) ;
}
2024-04-18 12:02:49 +08:00
AddInternal ( new HotkeyExitOverlay
{
Action = ( ) = >
{
if ( ! this . IsCurrentScreen ( ) ) return ;
this . Exit ( ) ;
} ,
} ) ;
2024-02-23 02:15:02 +08:00
if ( player ! = null & & AllowRetry )
2020-03-17 16:45:25 +08:00
{
2020-03-30 17:56:35 +08:00
buttons . Add ( new RetryButton { Width = 300 } ) ;
2020-03-29 22:52:50 +08:00
2020-03-30 17:56:35 +08:00
AddInternal ( new HotkeyRetryOverlay
{
Action = ( ) = >
2020-03-17 16:45:25 +08:00
{
2020-03-30 17:56:35 +08:00
if ( ! this . IsCurrentScreen ( ) ) return ;
2020-03-17 16:45:25 +08:00
2022-08-16 12:04:56 +08:00
player ? . Restart ( true ) ;
2020-03-30 17:56:35 +08:00
} ,
} ) ;
2020-03-17 16:45:25 +08:00
}
2020-03-17 16:43:16 +08:00
}
2020-05-16 18:00:20 +08:00
protected override void LoadComplete ( )
{
base . LoadComplete ( ) ;
2020-07-22 19:24:55 +08:00
var req = FetchScores ( fetchScoresCallback ) ;
2020-05-16 18:00:20 +08:00
2020-05-26 16:00:41 +08:00
if ( req ! = null )
api . Queue ( req ) ;
2020-06-18 21:27:27 +08:00
2023-08-01 18:00:01 +08:00
StatisticsPanel . State . BindValueChanged ( onStatisticsStateChanged , true ) ;
2020-05-16 18:00:20 +08:00
}
2020-07-22 19:24:55 +08:00
protected override void Update ( )
{
base . Update ( ) ;
2021-09-07 14:18:59 +08:00
if ( lastFetchCompleted )
2020-07-22 19:24:55 +08:00
{
2024-01-09 20:28:46 +08:00
APIRequest nextPageRequest = null ;
2021-09-07 14:18:59 +08:00
2024-01-09 20:28:46 +08:00
if ( ScorePanelList . IsScrolledToStart )
nextPageRequest = FetchNextPage ( - 1 , fetchScoresCallback ) ;
else if ( ScorePanelList . IsScrolledToEnd )
nextPageRequest = FetchNextPage ( 1 , fetchScoresCallback ) ;
2020-07-22 19:24:55 +08:00
2024-01-09 20:28:46 +08:00
if ( nextPageRequest ! = null )
{
lastFetchCompleted = false ;
api . Queue ( nextPageRequest ) ;
2020-07-22 19:24:55 +08:00
}
}
}
2020-05-26 16:00:41 +08:00
/// <summary>
/// Performs a fetch/refresh of scores to be displayed.
/// </summary>
/// <param name="scoresCallback">A callback which should be called when fetching is completed. Scheduling is not required.</param>
/// <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 ;
2020-07-28 19:58:13 +08:00
/// <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>
2020-07-22 19:24:55 +08:00
protected virtual APIRequest FetchNextPage ( int direction , Action < IEnumerable < ScoreInfo > > scoresCallback ) = > null ;
2022-12-24 17:35:42 +08:00
/// <summary>
2023-08-01 18:00:01 +08:00
/// Creates the <see cref="Statistics.StatisticsPanel"/> to be used to display extended information about scores.
2022-12-24 17:35:42 +08:00
/// </summary>
2024-02-23 02:49:14 +08:00
private StatisticsPanel createStatisticsPanel ( )
{
return ShowUserStatistics & & Score ! = null
? new UserStatisticsPanel ( Score )
: new StatisticsPanel ( ) ;
}
2022-12-24 17:35:42 +08:00
2020-07-22 19:24:55 +08:00
private void fetchScoresCallback ( IEnumerable < ScoreInfo > scores ) = > Schedule ( ( ) = >
{
foreach ( var s in scores )
addScore ( s ) ;
2021-09-07 14:18:59 +08:00
lastFetchCompleted = true ;
2024-01-09 20:28:46 +08:00
2024-01-16 01:47:49 +08:00
if ( ScorePanelList . IsEmpty )
2024-01-09 20:28:46 +08:00
{
// This can happen if for example a beatmap that is part of a playlist hasn't been played yet.
VerticalScrollContent . Add ( new MessagePlaceholder ( LeaderboardStrings . NoRecordsYet ) ) ;
}
2020-07-22 19:24:55 +08:00
} ) ;
2022-04-21 23:52:44 +08:00
public override void OnEntering ( ScreenTransitionEvent e )
2020-03-17 16:43:16 +08:00
{
2022-04-21 23:52:44 +08:00
base . OnEntering ( e ) ;
2020-03-17 16:43:16 +08:00
2021-01-04 17:32:23 +08:00
ApplyToBackground ( b = >
{
b . BlurAmount . Value = BACKGROUND_BLUR ;
2021-06-09 16:18:52 +08:00
b . FadeColour ( OsuColour . Gray ( 0.5f ) , 250 ) ;
2021-01-04 17:32:23 +08:00
} ) ;
2020-03-17 16:43:16 +08:00
bottomPanel . FadeTo ( 1 , 250 ) ;
2022-07-22 18:06:31 +08:00
popInSample ? . Play ( ) ;
2020-03-17 16:43:16 +08:00
}
2022-04-21 23:52:44 +08:00
public override bool OnExiting ( ScreenExitEvent e )
2020-07-14 15:00:43 +08:00
{
2022-04-21 23:52:44 +08:00
if ( base . OnExiting ( e ) )
2021-06-09 16:19:36 +08:00
return true ;
2020-07-14 15:00:43 +08:00
2024-01-29 14:39:31 +08:00
// This is a stop-gap safety against components holding references to gameplay after exiting the gameplay flow.
// Right now, HitEvents are only used up to the results screen. If this changes in the future we need to remove
// HitObject references from HitEvent.
2024-01-29 15:13:30 +08:00
Score ? . HitEvents . Clear ( ) ;
2024-01-29 14:39:31 +08:00
2021-06-09 16:19:36 +08:00
this . FadeOut ( 100 ) ;
return false ;
2020-07-14 15:00:43 +08:00
}
public override bool OnBackButton ( )
2020-03-17 16:43:16 +08:00
{
2023-08-01 18:00:01 +08:00
if ( StatisticsPanel . State . Value = = Visibility . Visible )
2020-06-18 21:27:27 +08:00
{
2023-08-01 18:00:01 +08:00
StatisticsPanel . Hide ( ) ;
2020-06-18 21:27:27 +08:00
return true ;
}
2020-07-14 15:00:43 +08:00
return false ;
2020-03-17 16:43:16 +08:00
}
2020-03-17 21:21:16 +08:00
2020-06-19 16:28:35 +08:00
private void addScore ( ScoreInfo score )
{
2020-07-31 18:57:05 +08:00
var panel = ScorePanelList . AddScore ( score ) ;
2020-06-19 16:28:35 +08:00
if ( detachedPanel ! = null )
panel . Alpha = 0 ;
}
private ScorePanel detachedPanel ;
2020-06-18 21:27:27 +08:00
private void onStatisticsStateChanged ( ValueChangedEvent < Visibility > state )
2020-03-17 21:21:16 +08:00
{
2020-06-19 16:28:35 +08:00
if ( state . NewValue = = Visibility . Visible )
2020-03-17 21:21:16 +08:00
{
2020-06-19 16:28:35 +08:00
// Detach the panel in its original location, and move into the desired location in the local container.
2020-07-31 18:57:05 +08:00
var expandedPanel = ScorePanelList . GetPanelForScore ( SelectedScore . Value ) ;
2020-06-19 16:28:35 +08:00
var screenSpacePos = expandedPanel . ScreenSpaceDrawQuad . TopLeft ;
2020-06-18 21:27:27 +08:00
2020-06-19 16:28:35 +08:00
// Detach and move into the local container.
2020-07-31 18:57:05 +08:00
ScorePanelList . Detach ( expandedPanel ) ;
2020-06-19 16:28:35 +08:00
detachedPanelContainer . Add ( expandedPanel ) ;
2020-06-22 14:42:55 +08:00
// Move into its original location in the local container first, then to the final location.
2021-10-27 12:04:41 +08:00
float origLocation = detachedPanelContainer . ToLocalSpace ( screenSpacePos ) . X ;
2020-09-24 11:49:32 +08:00
expandedPanel . MoveToX ( origLocation )
2020-06-22 14:42:55 +08:00
. Then ( )
2023-07-07 16:32:22 +08:00
. MoveToX ( StatisticsPanel . SIDE_PADDING , 400 , Easing . OutElasticQuarter ) ;
2020-06-19 16:28:35 +08:00
// Hide contracted panels.
2020-07-31 18:57:05 +08:00
foreach ( var contracted in ScorePanelList . GetScorePanels ( ) . Where ( p = > p . State = = PanelState . Contracted ) )
2020-06-19 16:28:35 +08:00
contracted . FadeOut ( 150 , Easing . OutQuint ) ;
2020-07-31 18:57:05 +08:00
ScorePanelList . HandleInput = false ;
2020-06-19 16:28:35 +08:00
// Dim background.
2023-07-07 16:32:22 +08:00
ApplyToBackground ( b = > b . FadeColour ( OsuColour . Gray ( 0.4f ) , 400 , Easing . OutQuint ) ) ;
2020-06-19 16:28:35 +08:00
detachedPanel = expandedPanel ;
2020-06-18 15:50:45 +08:00
}
2020-06-19 16:28:35 +08:00
else if ( detachedPanel ! = null )
2020-06-18 15:50:45 +08:00
{
2020-06-19 16:28:35 +08:00
var screenSpacePos = detachedPanel . ScreenSpaceDrawQuad . TopLeft ;
2020-06-18 21:27:27 +08:00
2020-06-19 16:28:35 +08:00
// Remove from the local container and re-attach.
2022-08-26 14:19:05 +08:00
detachedPanelContainer . Remove ( detachedPanel , false ) ;
2020-07-31 18:57:05 +08:00
ScorePanelList . Attach ( detachedPanel ) ;
2020-06-18 15:50:45 +08:00
2020-06-22 14:42:55 +08:00
// Move into its original location in the attached container first, then to the final location.
2023-10-17 16:40:44 +08:00
float origLocation = detachedPanel . Parent ! . ToLocalSpace ( screenSpacePos ) . X ;
2021-11-18 15:15:51 +08:00
detachedPanel . MoveToX ( origLocation )
2020-06-22 14:42:55 +08:00
. Then ( )
2023-07-07 16:32:22 +08:00
. MoveToX ( 0 , 250 , Easing . OutElasticQuarter ) ;
2020-06-19 16:28:35 +08:00
// Show contracted panels.
2020-07-31 18:57:05 +08:00
foreach ( var contracted in ScorePanelList . GetScorePanels ( ) . Where ( p = > p . State = = PanelState . Contracted ) )
2020-06-19 16:28:35 +08:00
contracted . FadeIn ( 150 , Easing . OutQuint ) ;
2020-07-31 18:57:05 +08:00
ScorePanelList . HandleInput = true ;
2020-06-19 16:28:35 +08:00
// Un-dim background.
2023-07-07 16:32:22 +08:00
ApplyToBackground ( b = > b . FadeColour ( OsuColour . Gray ( 0.5f ) , 250 , Easing . OutQuint ) ) ;
2020-06-19 16:28:35 +08:00
detachedPanel = null ;
2020-03-17 21:21:16 +08:00
}
}
2020-06-30 15:36:53 +08:00
2021-09-16 17:26:12 +08:00
public bool OnPressed ( KeyBindingPressEvent < GlobalAction > e )
2020-10-11 09:30:13 +08:00
{
2021-11-18 11:35:47 +08:00
if ( e . Repeat )
return false ;
2021-09-16 17:26:12 +08:00
switch ( e . Action )
2020-10-11 09:30:13 +08:00
{
case GlobalAction . Select :
2023-08-01 18:00:01 +08:00
StatisticsPanel . ToggleVisibility ( ) ;
2020-10-11 09:30:13 +08:00
return true ;
}
return false ;
}
2021-09-16 17:26:12 +08:00
public void OnReleased ( KeyBindingReleaseEvent < GlobalAction > e )
2020-10-11 09:30:13 +08:00
{
}
2021-08-13 15:14:23 +08:00
protected partial class VerticalScrollContainer : OsuScrollContainer
2020-06-30 15:36:53 +08:00
{
protected override Container < Drawable > Content = > content ;
private readonly Container content ;
public VerticalScrollContainer ( )
{
2021-08-13 15:14:23 +08:00
Masking = false ;
2020-06-30 15:36:53 +08:00
base . Content . Add ( content = new Container { RelativeSizeAxes = Axes . X } ) ;
}
protected override void Update ( )
{
base . Update ( ) ;
content . Height = Math . Max ( screen_height , DrawHeight ) ;
}
}
2020-03-17 16:43:16 +08:00
}
}