2019-06-04 15:13:16 +08:00
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
2019-01-24 16:43:03 +08:00
// See the LICENCE file in the repository root for full licence text.
2018-04-13 17:19:50 +08:00
2022-06-17 15:37:17 +08:00
#nullable disable
2018-01-21 23:11:43 +08:00
using System ;
2022-08-25 13:26:42 +08:00
using System.Diagnostics ;
2020-03-24 13:13:46 +08:00
using System.IO ;
2018-01-21 23:11:43 +08:00
using System.Linq ;
2022-06-09 13:03:21 +08:00
using System.Threading ;
2020-12-18 15:51:59 +08:00
using System.Threading.Tasks ;
2022-09-19 23:06:02 +08:00
using JetBrains.Annotations ;
2016-11-09 07:13:20 +08:00
using osu.Framework.Allocation ;
2019-02-21 18:04:31 +08:00
using osu.Framework.Bindables ;
2022-01-03 16:31:12 +08:00
using osu.Framework.Extensions ;
2016-11-14 16:23:33 +08:00
using osu.Framework.Graphics ;
2017-03-10 10:59:08 +08:00
using osu.Framework.Graphics.Containers ;
2018-10-02 11:02:47 +08:00
using osu.Framework.Input.Events ;
2017-03-10 10:59:08 +08:00
using osu.Framework.Logging ;
using osu.Framework.Screens ;
2017-04-07 15:55:41 +08:00
using osu.Framework.Threading ;
2022-05-07 22:17:23 +08:00
using osu.Game.Audio ;
2017-08-05 15:22:10 +08:00
using osu.Game.Beatmaps ;
2017-03-10 10:59:08 +08:00
using osu.Game.Configuration ;
2023-07-26 14:21:58 +08:00
using osu.Game.Database ;
2022-07-07 13:49:22 +08:00
using osu.Game.Extensions ;
2019-01-04 12:29:37 +08:00
using osu.Game.Graphics.Containers ;
2020-03-24 13:13:46 +08:00
using osu.Game.IO.Archives ;
2017-08-22 15:58:47 +08:00
using osu.Game.Online.API ;
2018-06-06 14:10:09 +08:00
using osu.Game.Overlays ;
2017-04-18 15:05:58 +08:00
using osu.Game.Rulesets ;
2017-04-21 16:33:20 +08:00
using osu.Game.Rulesets.Mods ;
2017-04-18 15:05:58 +08:00
using osu.Game.Rulesets.Scoring ;
2018-01-21 23:11:43 +08:00
using osu.Game.Rulesets.UI ;
2018-11-28 15:12:57 +08:00
using osu.Game.Scoring ;
2020-03-24 13:13:46 +08:00
using osu.Game.Scoring.Legacy ;
2022-09-13 15:36:09 +08:00
using osu.Game.Screens.Play.HUD ;
2018-01-21 23:11:43 +08:00
using osu.Game.Screens.Ranking ;
2018-03-20 15:26:36 +08:00
using osu.Game.Skinning ;
2019-04-13 04:54:35 +08:00
using osu.Game.Users ;
2024-01-15 19:57:17 +08:00
using osu.Game.Utils ;
2021-06-09 16:07:28 +08:00
using osuTK.Graphics ;
2018-04-13 17:19:50 +08:00
2016-11-14 16:23:33 +08:00
namespace osu.Game.Screens.Play
2016-09-29 19:13:58 +08:00
{
2019-11-01 14:32:06 +08:00
[Cached]
2021-08-17 15:13:45 +08:00
public abstract partial class Player : ScreenWithBeatmapBackground , ISamplePlaybackDisabler , ILocalUserPlayInfo
2016-09-29 19:13:58 +08:00
{
2020-04-20 11:42:33 +08:00
/// <summary>
/// The delay upon completion of the beatmap before displaying the results screen.
/// </summary>
public const double RESULTS_DISPLAY_DELAY = 1000.0 ;
2021-08-11 17:16:25 +08:00
/// <summary>
/// Raised after <see cref="StartGameplay"/> is called.
/// </summary>
public event Action OnGameplayStarted ;
2019-06-25 15:55:49 +08:00
public override bool AllowBackButton = > false ; // handled by HoldForMenuButton
2018-04-13 17:19:50 +08:00
2022-06-15 16:49:18 +08:00
protected override bool PlayExitSound = > ! isRestarting ;
2021-08-16 06:32:33 +08:00
protected override UserActivity InitialActivity = > new UserActivity . InSoloGame ( Beatmap . Value . BeatmapInfo , Ruleset . Value ) ;
2018-04-13 17:19:50 +08:00
2019-01-23 19:52:00 +08:00
public override float BackgroundParallaxAmount = > 0.1f ;
2018-04-13 17:19:50 +08:00
2019-01-28 14:41:54 +08:00
public override bool HideOverlaysOnEnter = > true ;
2018-04-13 17:19:50 +08:00
2022-10-20 08:44:58 +08:00
public override bool HideMenuCursorOnNonMouseInput = > true ;
2020-08-31 17:16:13 +08:00
protected override OverlayActivation InitialOverlayActivationMode = > OverlayActivation . UserTriggered ;
2020-08-09 03:21:30 +08:00
2020-09-01 15:55:10 +08:00
// We are managing our own adjustments (see OnEntering/OnExiting).
2023-07-25 18:58:23 +08:00
public override bool? ApplyModTrackAdjustments = > false ;
2018-06-06 14:10:09 +08:00
2021-02-05 14:07:59 +08:00
private readonly IBindable < bool > gameActive = new Bindable < bool > ( true ) ;
2020-10-14 18:39:48 +08:00
private readonly Bindable < bool > samplePlaybackDisabled = new Bindable < bool > ( ) ;
2018-06-06 14:10:09 +08:00
2019-05-10 14:39:25 +08:00
/// <summary>
/// Whether gameplay should pause when the game window focus is lost.
/// </summary>
protected virtual bool PauseOnFocusLost = > true ;
2018-06-06 14:10:09 +08:00
2022-08-16 12:04:56 +08:00
public Action < bool > RestartRequested ;
2018-04-13 17:19:50 +08:00
2022-06-15 16:49:18 +08:00
private bool isRestarting ;
2018-03-08 21:16:47 +08:00
private Bindable < bool > mouseWheelDisabled ;
2019-02-25 12:27:44 +08:00
private readonly Bindable < bool > storyboardReplacesBackground = new Bindable < bool > ( ) ;
2018-04-13 17:19:50 +08:00
2021-08-17 13:39:22 +08:00
public IBindable < bool > LocalUserPlaying = > localUserPlaying ;
private readonly Bindable < bool > localUserPlaying = new Bindable < bool > ( ) ;
2020-10-06 20:09:35 +08:00
2017-01-30 16:08:14 +08:00
public int RestartCount ;
2018-04-13 17:19:50 +08:00
2022-10-12 14:11:52 +08:00
/// <summary>
/// Whether the <see cref="HUDOverlay"/> is currently visible.
/// </summary>
public IBindable < bool > ShowingOverlayComponents = new Bindable < bool > ( ) ;
2024-04-23 20:54:44 +08:00
// Should match PlayerLoader for consistency. Cached here for the rare case we push a Player
// without the loading screen (one such usage is the skin editor's scene library).
[Cached]
private OverlayColourProvider colourProvider = new OverlayColourProvider ( OverlayColourScheme . Purple ) ;
2018-11-29 13:56:29 +08:00
[Resolved]
private ScoreManager scoreManager { get ; set ; }
2018-04-13 17:19:50 +08:00
2020-02-14 21:14:00 +08:00
[Resolved]
private IAPIProvider api { get ; set ; }
2018-04-13 17:19:50 +08:00
2020-09-01 17:07:19 +08:00
[Resolved]
private MusicController musicController { get ; set ; }
2020-08-03 03:34:35 +08:00
2023-12-21 17:14:24 +08:00
[Resolved]
private OsuGameBase game { get ; set ; }
2021-10-02 01:22:23 +08:00
public GameplayState GameplayState { get ; private set ; }
2021-05-31 18:22:20 +08:00
2021-10-02 01:22:23 +08:00
private Ruleset ruleset ;
2021-05-31 18:22:20 +08:00
2019-12-12 14:14:59 +08:00
public BreakOverlay BreakOverlay ;
2018-04-13 17:19:50 +08:00
2020-10-11 06:15:20 +08:00
/// <summary>
/// Whether the gameplay is currently in a break.
/// </summary>
2020-10-11 20:46:55 +08:00
public readonly IBindable < bool > IsBreakTime = new BindableBool ( ) ;
2020-10-10 21:07:17 +08:00
2020-03-26 14:28:56 +08:00
private BreakTracker breakTracker ;
2021-04-16 12:59:10 +08:00
private SkipOverlay skipIntroOverlay ;
2021-04-14 12:04:03 +08:00
private SkipOverlay skipOutroOverlay ;
2020-05-13 03:12:48 +08:00
2019-02-28 19:01:15 +08:00
protected ScoreProcessor ScoreProcessor { get ; private set ; }
2019-12-19 19:03:14 +08:00
protected HealthProcessor HealthProcessor { get ; private set ; }
2019-03-19 22:44:15 +08:00
protected DrawableRuleset DrawableRuleset { get ; private set ; }
2018-04-13 17:19:50 +08:00
2019-02-28 19:01:15 +08:00
protected HUDOverlay HUDOverlay { get ; private set ; }
2018-04-13 17:19:50 +08:00
2019-03-19 22:44:15 +08:00
public bool LoadedBeatmapSuccessfully = > DrawableRuleset ? . Objects . Any ( ) = = true ;
2018-04-13 17:19:50 +08:00
2019-03-17 23:46:15 +08:00
protected GameplayClockContainer GameplayClockContainer { get ; private set ; }
2018-04-13 17:19:50 +08:00
2019-11-25 15:24:29 +08:00
public DimmableStoryboard DimmableStoryboard { get ; private set ; }
2018-04-13 17:19:50 +08:00
2019-09-19 03:49:28 +08:00
/// <summary>
2019-09-19 12:58:54 +08:00
/// Whether failing should be allowed.
2019-09-19 13:31:11 +08:00
/// By default, this checks whether all selected mods allow failing.
2019-09-19 03:49:28 +08:00
/// </summary>
2021-10-07 13:53:36 +08:00
protected virtual bool CheckModsAllowFailure ( ) = > GameplayState . Mods . OfType < IApplicableFailOverride > ( ) . All ( m = > m . PerformFail ( ) ) ;
2019-09-19 03:49:28 +08:00
2020-12-23 16:39:08 +08:00
public readonly PlayerConfiguration Configuration ;
2019-03-26 15:53:44 +08:00
2022-03-01 13:47:06 +08:00
/// <summary>
/// The score for the current play session.
/// Available only after the player is loaded.
/// </summary>
public Score Score { get ; private set ; }
2021-06-02 14:44:04 +08:00
2019-03-26 15:53:44 +08:00
/// <summary>
/// Create a new player instance.
/// </summary>
2021-03-23 13:47:15 +08:00
protected Player ( PlayerConfiguration configuration = null )
2019-03-26 15:53:44 +08:00
{
2020-12-23 17:07:38 +08:00
Configuration = configuration ? ? new PlayerConfiguration ( ) ;
2019-03-26 15:53:44 +08:00
}
2018-04-13 17:19:50 +08:00
2020-06-18 22:35:03 +08:00
private ScreenSuspensionHandler screenSuspension ;
2020-02-14 11:30:11 +08:00
private DependencyContainer dependencies ;
protected override IReadOnlyDependencyContainer CreateChildDependencies ( IReadOnlyDependencyContainer parent )
= > dependencies = new DependencyContainer ( base . CreateChildDependencies ( parent ) ) ;
2020-03-23 18:31:43 +08:00
protected override void LoadComplete ( )
{
base . LoadComplete ( ) ;
2021-05-14 15:53:51 +08:00
if ( ! LoadedBeatmapSuccessfully )
2021-05-14 14:10:02 +08:00
return ;
2021-05-25 17:09:37 +08:00
PrepareReplay ( ) ;
2021-02-05 14:07:59 +08:00
2022-06-24 20:25:23 +08:00
ScoreProcessor . NewJudgement + = _ = > ScoreProcessor . PopulateScore ( Score . ScoreInfo ) ;
2022-01-31 17:54:23 +08:00
ScoreProcessor . OnResetFromReplayFrame + = ( ) = > ScoreProcessor . PopulateScore ( Score . ScoreInfo ) ;
2021-02-05 14:07:59 +08:00
gameActive . BindValueChanged ( _ = > updatePauseOnFocusLostState ( ) , true ) ;
2020-03-23 18:31:43 +08:00
}
/// <summary>
/// Run any recording / playback setup for replays.
/// </summary>
protected virtual void PrepareReplay ( )
{
2021-06-02 14:44:04 +08:00
DrawableRuleset . SetRecordTarget ( Score ) ;
}
2020-12-14 16:33:33 +08:00
2020-10-07 13:46:58 +08:00
[BackgroundDependencyLoader(true)]
2023-07-06 11:25:15 +08:00
private void load ( OsuConfigManager config , OsuGameBase game , CancellationToken cancellationToken )
2016-10-05 19:49:31 +08:00
{
2021-10-07 13:53:36 +08:00
var gameplayMods = Mods . Value . Select ( m = > m . DeepClone ( ) ) . ToArray ( ) ;
2019-04-09 12:33:16 +08:00
2022-03-09 16:38:56 +08:00
if ( gameplayMods . Any ( m = > m is UnknownMod ) )
{
Logger . Log ( "Gameplay was started with an unknown mod applied." , level : LogLevel . Important ) ;
return ;
}
2019-12-12 14:58:11 +08:00
if ( Beatmap . Value is DummyWorkingBeatmap )
return ;
2022-06-09 13:03:21 +08:00
IBeatmap playableBeatmap = loadPlayableBeatmap ( gameplayMods , cancellationToken ) ;
2019-03-06 19:30:14 +08:00
2019-12-12 14:58:11 +08:00
if ( playableBeatmap = = null )
2018-03-23 04:46:35 +08:00
return ;
2018-04-13 17:19:50 +08:00
2024-01-15 19:57:17 +08:00
if ( ! ModUtils . CheckModsBelongToRuleset ( ruleset , gameplayMods ) )
2024-01-13 20:52:57 +08:00
{
Logger . Log ( $@"Gameplay was started with a mod belonging to a ruleset different than '{ruleset.Description}'." , level : LogLevel . Important ) ;
return ;
}
2018-03-25 19:25:48 +08:00
mouseWheelDisabled = config . GetBindable < bool > ( OsuSetting . MouseDisableWheel ) ;
2018-04-13 17:19:50 +08:00
2020-10-07 13:46:58 +08:00
if ( game ! = null )
2021-02-08 14:58:41 +08:00
gameActive . BindTo ( game . IsActive ) ;
2021-02-08 19:05:16 +08:00
2021-02-08 18:59:07 +08:00
if ( game is OsuGame osuGame )
LocalUserPlaying . BindTo ( osuGame . LocalUserPlaying ) ;
2020-10-06 20:09:35 +08:00
2021-10-07 13:53:36 +08:00
DrawableRuleset = ruleset . CreateDrawableRulesetWith ( playableBeatmap , gameplayMods ) ;
2021-05-17 17:22:24 +08:00
dependencies . CacheAs ( DrawableRuleset ) ;
2019-12-12 14:58:11 +08:00
2021-10-02 01:22:23 +08:00
ScoreProcessor = ruleset . CreateScoreProcessor ( ) ;
2021-10-07 13:53:36 +08:00
ScoreProcessor . Mods . Value = gameplayMods ;
2022-05-30 18:11:54 +08:00
ScoreProcessor . ApplyBeatmap ( playableBeatmap ) ;
2018-04-13 17:19:50 +08:00
2021-05-03 15:47:47 +08:00
dependencies . CacheAs ( ScoreProcessor ) ;
2023-12-17 18:27:03 +08:00
HealthProcessor = gameplayMods . OfType < IApplicableHealthProcessor > ( ) . FirstOrDefault ( ) ? . CreateHealthProcessor ( playableBeatmap . HitObjects [ 0 ] . StartTime ) ;
HealthProcessor ? ? = ruleset . CreateHealthProcessor ( playableBeatmap . HitObjects [ 0 ] . StartTime ) ;
2019-12-24 16:01:17 +08:00
HealthProcessor . ApplyBeatmap ( playableBeatmap ) ;
2019-12-19 19:03:14 +08:00
2021-05-07 15:56:24 +08:00
dependencies . CacheAs ( HealthProcessor ) ;
2020-10-27 17:56:28 +08:00
InternalChild = GameplayClockContainer = CreateGameplayClockContainer ( Beatmap . Value , DrawableRuleset . GameplayStartTime ) ;
2018-04-13 17:19:50 +08:00
2020-06-18 22:35:03 +08:00
AddInternal ( screenSuspension = new ScreenSuspensionHandler ( GameplayClockContainer ) ) ;
2021-10-07 19:52:36 +08:00
2021-10-05 13:48:10 +08:00
Score = CreateScore ( playableBeatmap ) ;
2020-02-14 11:30:11 +08:00
2021-10-05 13:48:10 +08:00
// ensure the score is in a consistent state with the current player.
Score . ScoreInfo . BeatmapInfo = Beatmap . Value . BeatmapInfo ;
2023-02-07 16:52:47 +08:00
Score . ScoreInfo . BeatmapHash = Beatmap . Value . BeatmapInfo . Hash ;
2021-10-05 13:48:10 +08:00
Score . ScoreInfo . Ruleset = ruleset . RulesetInfo ;
2021-10-07 13:53:36 +08:00
Score . ScoreInfo . Mods = gameplayMods ;
2021-10-05 13:48:10 +08:00
2024-06-14 19:36:40 +08:00
dependencies . CacheAs ( GameplayState = new GameplayState ( playableBeatmap , ruleset , gameplayMods , Score , ScoreProcessor , HealthProcessor , Beatmap . Value . Storyboard ) ) ;
2020-02-14 11:30:11 +08:00
2021-10-02 01:22:23 +08:00
var rulesetSkinProvider = new RulesetSkinProvidingContainer ( ruleset , playableBeatmap , Beatmap . Value . Skin ) ;
2020-09-29 13:09:51 +08:00
// load the skinning hierarchy first.
// this is intentionally done in two stages to ensure things are in a loaded state before exposing the ruleset to skin sources.
2021-06-10 02:30:26 +08:00
GameplayClockContainer . Add ( rulesetSkinProvider ) ;
2020-09-29 13:09:51 +08:00
2022-08-09 22:25:19 +08:00
if ( cancellationToken . IsCancellationRequested )
return ;
2021-10-15 18:35:08 +08:00
rulesetSkinProvider . AddRange ( new Drawable [ ]
2020-09-29 13:09:51 +08:00
{
2023-10-07 23:39:30 +08:00
failAnimationContainer = new FailAnimationContainer ( DrawableRuleset )
2021-10-15 18:35:08 +08:00
{
OnComplete = onFailComplete ,
Children = new [ ]
{
// underlay and gameplay should have access to the skinning sources.
createUnderlayComponents ( ) ,
2022-02-02 04:32:58 +08:00
createGameplayComponents ( Beatmap . Value )
2021-10-15 18:35:08 +08:00
}
} ,
2024-02-28 14:41:49 +08:00
FailOverlay = new FailOverlay
2021-10-15 18:35:08 +08:00
{
2023-02-14 15:55:35 +08:00
SaveReplay = async ( ) = > await prepareAndImportScoreAsync ( true ) . ConfigureAwait ( false ) ,
2024-02-28 14:41:49 +08:00
OnRetry = Configuration . AllowUserInteraction ? ( ) = > Restart ( ) : null ,
2021-10-15 18:35:08 +08:00
OnQuit = ( ) = > PerformExit ( true ) ,
2021-10-16 01:29:45 +08:00
} ,
new HotkeyExitOverlay
{
Action = ( ) = >
{
if ( ! this . IsCurrentScreen ( ) ) return ;
2023-10-12 14:42:50 +08:00
if ( PerformExit ( false ) )
// The hotkey overlay dims the screen.
// If the operation succeeds, we want to make sure we stay dimmed to keep continuity.
fadeOut ( true ) ;
2021-10-16 01:29:45 +08:00
} ,
} ,
2020-09-29 13:09:51 +08:00
} ) ;
2022-08-09 22:25:19 +08:00
if ( cancellationToken . IsCancellationRequested )
return ;
2021-10-16 01:29:45 +08:00
if ( Configuration . AllowRestart )
{
2023-07-06 11:25:15 +08:00
rulesetSkinProvider . AddRange ( new Drawable [ ]
2021-10-16 01:29:45 +08:00
{
2023-07-06 11:25:15 +08:00
new HotkeyRetryOverlay
2021-10-16 01:29:45 +08:00
{
2023-07-06 11:25:15 +08:00
Action = ( ) = >
{
if ( ! this . IsCurrentScreen ( ) ) return ;
2021-10-16 01:29:45 +08:00
2023-10-12 14:42:50 +08:00
if ( Restart ( true ) )
// The hotkey overlay dims the screen.
// If the operation succeeds, we want to make sure we stay dimmed to keep continuity.
fadeOut ( true ) ;
2023-07-06 11:25:15 +08:00
} ,
2021-10-16 01:29:45 +08:00
} ,
} ) ;
}
2023-01-18 16:19:57 +08:00
dependencies . CacheAs ( DrawableRuleset . FrameStableClock ) ;
2020-09-29 13:09:51 +08:00
// add the overlay components as a separate step as they proxy some elements from the above underlay/gameplay components.
2021-06-10 03:24:53 +08:00
// also give the overlays the ruleset skin provider to allow rulesets to potentially override HUD elements (used to disable combo counters etc.)
// we may want to limit this in the future to disallow rulesets from outright replacing elements the user expects to be there.
2023-10-07 23:39:30 +08:00
failAnimationContainer . Add ( createOverlayComponents ( Beatmap . Value ) ) ;
2018-04-13 17:19:50 +08:00
2020-05-08 15:37:50 +08:00
if ( ! DrawableRuleset . AllowGameplayOverlays )
2020-05-07 14:52:36 +08:00
{
HUDOverlay . ShowHud . Value = false ;
HUDOverlay . ShowHud . Disabled = true ;
BreakOverlay . Hide ( ) ;
}
2020-10-27 17:13:45 +08:00
DrawableRuleset . FrameStableClock . WaitingOnFrames . BindValueChanged ( waiting = >
{
if ( waiting . NewValue )
GameplayClockContainer . Stop ( ) ;
else
GameplayClockContainer . Start ( ) ;
} ) ;
2022-06-24 20:25:23 +08:00
DrawableRuleset . IsPaused . BindValueChanged ( _ = >
2020-10-27 13:10:12 +08:00
{
updateGameplayState ( ) ;
updateSampleDisabledState ( ) ;
} ) ;
DrawableRuleset . FrameStableClock . IsCatchingUp . BindValueChanged ( _ = > updateSampleDisabledState ( ) ) ;
2020-10-27 12:54:33 +08:00
2020-10-06 20:09:35 +08:00
DrawableRuleset . HasReplayLoaded . BindValueChanged ( _ = > updateGameplayState ( ) ) ;
2020-08-16 23:18:40 +08:00
2019-08-27 17:27:21 +08:00
// bind clock into components that require it
2022-08-15 16:36:18 +08:00
( ( IBindable < bool > ) DrawableRuleset . IsPaused ) . BindTo ( GameplayClockContainer . IsPaused ) ;
2018-04-13 17:19:50 +08:00
2020-11-10 22:32:30 +08:00
DrawableRuleset . NewResult + = r = >
2019-12-19 19:03:14 +08:00
{
HealthProcessor . ApplyResult ( r ) ;
ScoreProcessor . ApplyResult ( r ) ;
2021-10-02 01:22:23 +08:00
GameplayState . ApplyResult ( r ) ;
2019-12-19 19:03:14 +08:00
} ;
2020-11-10 22:32:30 +08:00
DrawableRuleset . RevertResult + = r = >
2019-12-19 19:03:14 +08:00
{
HealthProcessor . RevertResult ( r ) ;
ScoreProcessor . RevertResult ( r ) ;
} ;
2019-12-11 16:25:06 +08:00
2023-03-31 01:30:04 +08:00
DimmableStoryboard . HasStoryboardEnded . ValueChanged + = _ = > checkScoreCompleted ( ) ;
2021-04-18 09:49:07 +08:00
2019-12-19 19:03:14 +08:00
// Bind the judgement processors to ourselves
2023-03-31 01:30:04 +08:00
ScoreProcessor . HasCompleted . BindValueChanged ( _ = > checkScoreCompleted ( ) ) ;
2019-12-19 19:03:14 +08:00
HealthProcessor . Failed + = onFail ;
2018-04-13 17:19:50 +08:00
2021-08-02 02:14:54 +08:00
// Provide judgement processors to mods after they're loaded so that they're on the gameplay clock,
// this is required for mods that apply transforms to these processors.
2021-08-02 00:16:30 +08:00
ScoreProcessor . OnLoadComplete + = _ = >
{
2021-10-07 13:53:36 +08:00
foreach ( var mod in gameplayMods . OfType < IApplicableToScoreProcessor > ( ) )
2021-08-02 00:16:30 +08:00
mod . ApplyToScoreProcessor ( ScoreProcessor ) ;
} ;
2019-12-19 19:03:14 +08:00
2021-08-02 00:16:30 +08:00
HealthProcessor . OnLoadComplete + = _ = >
{
2021-10-07 13:53:36 +08:00
foreach ( var mod in gameplayMods . OfType < IApplicableToHealthProcessor > ( ) )
2021-08-02 00:16:30 +08:00
mod . ApplyToHealthProcessor ( HealthProcessor ) ;
} ;
2019-12-19 19:03:14 +08:00
2020-10-11 20:46:55 +08:00
IsBreakTime . BindTo ( breakTracker . IsBreakTime ) ;
2020-10-11 20:51:48 +08:00
IsBreakTime . BindValueChanged ( onBreakTimeChanged , true ) ;
2022-08-06 05:21:03 +08:00
2022-09-13 15:36:09 +08:00
loadLeaderboard ( ) ;
2019-08-27 17:27:21 +08:00
}
2018-04-13 17:19:50 +08:00
2021-04-14 16:47:11 +08:00
protected virtual GameplayClockContainer CreateGameplayClockContainer ( WorkingBeatmap beatmap , double gameplayStart ) = > new MasterGameplayClockContainer ( beatmap , gameplayStart ) ;
2020-10-27 17:56:28 +08:00
2020-09-29 13:09:51 +08:00
private Drawable createUnderlayComponents ( ) = >
2024-02-29 10:39:36 +08:00
DimmableStoryboard = new DimmableStoryboard ( GameplayState . Storyboard , GameplayState . Mods ) { RelativeSizeAxes = Axes . Both } ;
2018-04-13 17:19:50 +08:00
2022-02-02 04:32:58 +08:00
private Drawable createGameplayComponents ( IWorkingBeatmap working ) = > new ScalingContainer ( ScalingMode . Gameplay )
2019-08-27 17:27:21 +08:00
{
2020-09-29 13:09:51 +08:00
Children = new Drawable [ ]
2019-08-26 11:21:49 +08:00
{
2020-09-29 13:09:51 +08:00
DrawableRuleset . With ( r = >
r . FrameStableComponents . Children = new Drawable [ ]
{
ScoreProcessor ,
HealthProcessor ,
2020-11-13 12:35:01 +08:00
new ComboEffects ( ScoreProcessor ) ,
2020-09-29 13:09:51 +08:00
breakTracker = new BreakTracker ( DrawableRuleset . GameplayStartTime , ScoreProcessor )
{
Breaks = working . Beatmap . Breaks
}
} ) ,
}
} ;
2018-04-13 17:19:50 +08:00
2021-11-15 17:46:11 +08:00
private Drawable createOverlayComponents ( IWorkingBeatmap working )
2019-08-27 17:27:21 +08:00
{
2020-12-23 16:39:08 +08:00
var container = new Container
2016-10-06 22:33:09 +08:00
{
2020-12-23 16:39:08 +08:00
RelativeSizeAxes = Axes . Both ,
2021-10-15 18:35:08 +08:00
Children = new [ ]
2020-03-28 04:19:49 +08:00
{
2021-10-15 18:35:08 +08:00
DimmableStoryboard . OverlayLayerContainer . CreateProxy ( ) ,
BreakOverlay = new BreakOverlay ( working . Beatmap . BeatmapInfo . LetterboxInBreaks , ScoreProcessor )
2021-10-15 18:14:59 +08:00
{
2021-10-15 18:35:08 +08:00
Clock = DrawableRuleset . FrameStableClock ,
ProcessCustomClock = false ,
Breaks = working . Beatmap . Breaks
} ,
// display the cursor above some HUD elements.
DrawableRuleset . Cursor ? . CreateProxy ( ) ? ? new Container ( ) ,
DrawableRuleset . ResumeOverlay ? . CreateProxy ( ) ? ? new Container ( ) ,
2022-09-13 17:23:47 +08:00
HUDOverlay = new HUDOverlay ( DrawableRuleset , GameplayState . Mods , Configuration . AlwaysShowLeaderboard )
2021-10-15 18:35:08 +08:00
{
HoldToQuit =
2021-10-15 18:14:59 +08:00
{
2021-10-15 18:35:08 +08:00
Action = ( ) = > PerformExit ( true ) ,
2023-01-02 10:00:39 +08:00
IsPaused = { BindTarget = GameplayClockContainer . IsPaused } ,
ReplayLoaded = { BindTarget = DrawableRuleset . HasReplayLoaded } ,
2021-10-15 18:35:08 +08:00
} ,
2023-06-27 01:27:42 +08:00
InputCountController =
2021-10-15 18:35:08 +08:00
{
2023-02-22 22:58:27 +08:00
IsCounting =
{
Value = false
} ,
2021-10-15 18:35:08 +08:00
} ,
Anchor = Anchor . Centre ,
Origin = Anchor . Centre
} ,
skipIntroOverlay = new SkipOverlay ( DrawableRuleset . GameplayStartTime )
{
RequestSkip = performUserRequestedSkip
2021-10-15 18:14:59 +08:00
} ,
2024-02-29 10:39:36 +08:00
skipOutroOverlay = new SkipOverlay ( GameplayState . Storyboard . LatestEventTime ? ? 0 )
2021-10-15 18:14:59 +08:00
{
2021-10-15 18:35:08 +08:00
RequestSkip = ( ) = > progressToResults ( false ) ,
Alpha = 0
} ,
PauseOverlay = new PauseOverlay
{
OnResume = Resume ,
Retries = RestartCount ,
2022-08-16 12:04:56 +08:00
OnRetry = ( ) = > Restart ( ) ,
2021-10-15 18:14:59 +08:00
OnQuit = ( ) = > PerformExit ( true ) ,
2021-10-15 18:35:08 +08:00
} ,
} ,
2020-12-23 16:39:08 +08:00
} ;
2021-05-04 15:36:05 +08:00
if ( ! Configuration . AllowSkipping | | ! DrawableRuleset . AllowGameplayOverlays )
2021-04-16 13:03:15 +08:00
{
2021-04-16 12:59:10 +08:00
skipIntroOverlay . Expire ( ) ;
2021-04-16 13:03:15 +08:00
skipOutroOverlay . Expire ( ) ;
}
2021-04-14 12:04:03 +08:00
2020-12-23 16:39:08 +08:00
return container ;
}
2018-04-13 17:19:50 +08:00
2020-03-06 17:00:17 +08:00
private void onBreakTimeChanged ( ValueChangedEvent < bool > isBreakTime )
2020-02-29 23:37:42 +08:00
{
2020-10-11 20:51:48 +08:00
updateGameplayState ( ) ;
2020-03-06 17:00:17 +08:00
updatePauseOnFocusLostState ( ) ;
2023-06-27 01:27:42 +08:00
HUDOverlay . InputCountController . IsCounting . Value = ! isBreakTime . NewValue ;
2020-02-29 23:37:42 +08:00
}
2020-10-06 20:09:35 +08:00
private void updateGameplayState ( )
2020-08-04 03:25:45 +08:00
{
2022-04-26 10:19:19 +08:00
bool inGameplay = ! DrawableRuleset . HasReplayLoaded . Value & & ! DrawableRuleset . IsPaused . Value & & ! breakTracker . IsBreakTime . Value & & ! GameplayState . HasFailed ;
2020-10-06 20:09:35 +08:00
OverlayActivationMode . Value = inGameplay ? OverlayActivation . Disabled : OverlayActivation . UserTriggered ;
2021-08-17 13:39:22 +08:00
localUserPlaying . Value = inGameplay ;
2020-08-04 03:25:45 +08:00
}
2020-10-27 13:10:12 +08:00
private void updateSampleDisabledState ( )
{
2022-08-15 16:06:24 +08:00
samplePlaybackDisabled . Value = DrawableRuleset . FrameStableClock . IsCatchingUp . Value | | GameplayClockContainer . IsPaused . Value ;
2020-10-27 13:10:12 +08:00
}
2021-02-05 14:07:59 +08:00
private void updatePauseOnFocusLostState ( )
{
2021-02-22 21:59:35 +08:00
if ( ! PauseOnFocusLost | | ! pausingSupportedByCurrentState | | breakTracker . IsBreakTime . Value )
2021-02-05 14:07:59 +08:00
return ;
if ( gameActive . Value = = false )
2021-02-19 14:35:29 +08:00
{
2021-02-22 15:03:27 +08:00
bool paused = Pause ( ) ;
2021-02-23 12:23:32 +08:00
// if the initial pause could not be satisfied, the pause cooldown may be active.
// reschedule the pause attempt until it can be achieved.
2021-02-22 15:03:27 +08:00
if ( ! paused )
2021-02-19 16:33:26 +08:00
Scheduler . AddOnce ( updatePauseOnFocusLostState ) ;
2021-02-19 14:35:29 +08:00
}
2021-02-05 14:07:59 +08:00
}
2019-12-11 14:45:50 +08:00
2022-06-09 13:03:21 +08:00
private IBeatmap loadPlayableBeatmap ( Mod [ ] gameplayMods , CancellationToken cancellationToken )
2017-11-11 12:00:54 +08:00
{
2019-12-12 14:58:11 +08:00
IBeatmap playable ;
2019-03-06 19:30:14 +08:00
try
{
2019-12-12 14:58:11 +08:00
if ( Beatmap . Value . Beatmap = = null )
2019-03-06 19:30:14 +08:00
throw new InvalidOperationException ( "Beatmap was not loaded" ) ;
2018-04-13 17:19:50 +08:00
2021-05-31 18:22:20 +08:00
var rulesetInfo = Ruleset . Value ? ? Beatmap . Value . BeatmapInfo . Ruleset ;
2021-10-02 01:22:23 +08:00
ruleset = rulesetInfo . CreateInstance ( ) ;
2019-03-06 19:30:14 +08:00
2021-11-24 11:16:08 +08:00
if ( ruleset = = null )
throw new RulesetLoadException ( "Instantiation failure" ) ;
2019-03-06 19:30:14 +08:00
try
{
2022-06-09 13:03:21 +08:00
playable = Beatmap . Value . GetPlayableBeatmap ( ruleset . RulesetInfo , gameplayMods , cancellationToken ) ;
2019-03-06 19:30:14 +08:00
}
catch ( BeatmapInvalidForRulesetException )
{
2019-12-12 14:58:11 +08:00
// A playable beatmap may not be creatable with the user's preferred ruleset, so try using the beatmap's default ruleset
rulesetInfo = Beatmap . Value . BeatmapInfo . Ruleset ;
2021-10-02 01:22:23 +08:00
ruleset = rulesetInfo . CreateInstance ( ) ;
2019-12-12 14:58:11 +08:00
2022-06-09 13:03:21 +08:00
playable = Beatmap . Value . GetPlayableBeatmap ( rulesetInfo , gameplayMods , cancellationToken ) ;
2017-04-06 14:34:52 +08:00
}
2018-04-13 17:19:50 +08:00
2019-12-12 14:58:11 +08:00
if ( playable . HitObjects . Count = = 0 )
2019-03-06 19:30:14 +08:00
{
Fix `SkinEditorOverlay` freezing when `ReplayPlayer` screen exits early
Originally when popping in, the ReplayPlayer was loaded first (if previous screen was MainMenu), and afterwards the SkinEditor component was loaded asynchronously. However, if the ReplayPlayer screen exits quickly (like in the event the beatmap has no objects), the skin editor component has not finished initializing (this is before it was even added to the component tree, so it's still not marked `Visible`), then the screen exiting will cause `OsuGame` to call SetTarget(newScreen) -> setTarget(...) which sees that the cached `skinEditor` is not visible yet, and hides/nulls the field. This is the point where LoadComponentAsync(editor, ...) finishes, and the callback sees that the cached skinEditor field is now different (null) than the one that was loaded, and never adds it to the component tree. This occurrence is unhandled and as such the SkinEditorOverlay never hides itself, consuming all input infinitely.
This PR changes the loading to start loading the ReplayPlayer *after* the SkinEditor has been loaded and added to the component tree.
Additionally, this lowers the exit delay for ReplayPlayer and changes the "no hit objects" notification to not be an error since it's a controlled exit.
2023-12-27 03:08:21 +08:00
Logger . Log ( "Beatmap contains no hit objects!" , level : LogLevel . Important ) ;
2019-03-06 19:30:14 +08:00
return null ;
}
}
2022-06-09 13:35:52 +08:00
catch ( OperationCanceledException )
{
// Load has been cancelled. No logging is required.
return null ;
}
2019-03-06 19:30:14 +08:00
catch ( Exception e )
{
2022-06-09 13:35:52 +08:00
Logger . Error ( e , "Could not load beatmap successfully!" ) ;
2019-03-06 19:30:14 +08:00
//couldn't load, hard abort!
return null ;
}
2018-04-22 01:21:09 +08:00
2019-12-12 14:58:11 +08:00
return playable ;
2017-11-11 12:00:54 +08:00
}
2018-07-19 00:18:07 +08:00
2020-12-23 22:51:26 +08:00
/// <summary>
2021-06-17 17:10:59 +08:00
/// Attempts to complete a user request to exit gameplay.
2020-12-23 22:51:26 +08:00
/// </summary>
2021-06-17 17:10:59 +08:00
/// <remarks>
2021-06-17 21:26:50 +08:00
/// <list type="bullet">
/// <item>This should only be called in response to a user interaction. Exiting is not guaranteed.</item>
/// <item>This will interrupt any pending progression to the results screen, even if the transition has begun.</item>
/// </list>
2021-06-17 17:10:59 +08:00
/// </remarks>
2021-02-09 16:14:16 +08:00
/// <param name="showDialogFirst">
/// Whether the pause or fail dialog should be shown before performing an exit.
2021-06-17 22:04:58 +08:00
/// If <see langword="true"/> and a dialog is not yet displayed, the exit will be blocked and the relevant dialog will display instead.
2020-12-23 22:51:26 +08:00
/// </param>
2023-10-12 14:42:50 +08:00
/// <returns>Whether this call resulted in a final exit.</returns>
protected bool PerformExit ( bool showDialogFirst )
2018-12-13 15:17:24 +08:00
{
2021-02-19 16:26:54 +08:00
bool pauseOrFailDialogVisible =
PauseOverlay . State . Value = = Visibility . Visible | | FailOverlay . State . Value = = Visibility . Visible ;
2020-12-23 22:51:26 +08:00
2021-02-19 16:26:54 +08:00
if ( showDialogFirst & & ! pauseOrFailDialogVisible )
2021-02-09 15:24:29 +08:00
{
2021-02-15 14:57:21 +08:00
// if the fail animation is currently in progress, accelerate it (it will show the pause dialog on completion).
2022-01-26 00:45:11 +08:00
if ( ValidForResume & & GameplayState . HasFailed )
2021-02-09 15:24:29 +08:00
{
2023-10-07 23:39:30 +08:00
failAnimationContainer . FinishTransforms ( true ) ;
2023-10-12 14:42:50 +08:00
return false ;
2021-02-09 15:24:29 +08:00
}
2021-06-17 17:10:59 +08:00
// even if this call has requested a dialog, there is a chance the current player mode doesn't support pausing.
2021-02-20 12:35:25 +08:00
if ( pausingSupportedByCurrentState )
{
// in the case a dialog needs to be shown, attempt to pause and show it.
// this may fail (see internal checks in Pause()) but the fail cases are temporary, so don't fall through to Exit().
Pause ( ) ;
2023-10-12 14:42:50 +08:00
return false ;
2021-02-20 12:35:25 +08:00
}
2021-02-09 15:24:29 +08:00
}
2023-10-12 13:42:45 +08:00
// Matching osu!stable behaviour, if the results screen is pending and the user requests an exit,
// show the results instead.
2023-10-12 18:26:32 +08:00
if ( GameplayState . HasPassed & & ! isRestarting )
2023-10-12 13:42:45 +08:00
{
progressToResults ( false ) ;
2023-10-12 14:42:50 +08:00
return false ;
2023-10-12 13:42:45 +08:00
}
2022-10-02 20:30:06 +08:00
2023-02-06 14:59:37 +08:00
// import current score if possible.
2023-02-14 15:55:35 +08:00
prepareAndImportScoreAsync ( ) ;
2023-02-05 23:35:11 +08:00
2023-10-12 18:09:43 +08:00
// Screen may not be current if a restart has been performed.
if ( this . IsCurrentScreen ( ) )
{
// The actual exit is performed if
// - the pause / fail dialog was not requested
// - the pause / fail dialog was requested but is already displayed (user showing intention to exit).
// - the pause / fail dialog was requested but couldn't be displayed due to the type or state of this Player instance.
this . Exit ( ) ;
}
2023-10-12 14:42:50 +08:00
return true ;
2017-05-16 16:55:35 +08:00
}
2018-04-13 17:19:50 +08:00
2021-01-22 06:10:11 +08:00
private void performUserRequestedSkip ( )
{
// user requested skip
// disable sample playback to stop currently playing samples and perform skip
samplePlaybackDisabled . Value = true ;
2021-04-14 16:47:11 +08:00
( GameplayClockContainer as MasterGameplayClockContainer ) ? . Skip ( ) ;
2021-01-22 06:10:11 +08:00
// return samplePlaybackDisabled.Value to what is defined by the beatmap's current state
updateSampleDisabledState ( ) ;
}
2021-05-17 17:41:56 +08:00
/// <summary>
/// Seek to a specific time in gameplay.
/// </summary>
/// <param name="time">The destination time to seek to.</param>
public void Seek ( double time ) = > GameplayClockContainer . Seek ( time ) ;
2021-06-03 16:47:22 +08:00
private ScheduledDelegate frameStablePlaybackResetDelegate ;
2021-06-03 16:27:21 +08:00
/// <summary>
2022-03-17 19:54:42 +08:00
/// Specify and seek to a custom start time from which gameplay should be observed.
2021-06-03 16:27:21 +08:00
/// </summary>
/// <remarks>
2022-04-13 11:24:47 +08:00
/// This performs a non-frame-stable seek. Intermediate hitobject judgements may not be applied or reverted correctly during this seek.
2021-06-03 16:27:21 +08:00
/// </remarks>
/// <param name="time">The destination time to seek to.</param>
2022-03-17 19:54:42 +08:00
protected void SetGameplayStartTime ( double time )
2021-06-03 16:27:21 +08:00
{
2021-06-03 16:47:22 +08:00
if ( frameStablePlaybackResetDelegate ? . Cancelled = = false & & ! frameStablePlaybackResetDelegate . Completed )
frameStablePlaybackResetDelegate . RunTask ( ) ;
2021-06-03 16:27:21 +08:00
bool wasFrameStable = DrawableRuleset . FrameStablePlayback ;
DrawableRuleset . FrameStablePlayback = false ;
2022-08-22 13:11:06 +08:00
GameplayClockContainer . Reset ( time ) ;
2021-06-03 16:27:21 +08:00
// Delay resetting frame-stable playback for one frame to give the FrameStabilityContainer a chance to seek.
2021-06-03 16:47:22 +08:00
frameStablePlaybackResetDelegate = ScheduleAfterChildren ( ( ) = > DrawableRuleset . FrameStablePlayback = wasFrameStable ) ;
2021-06-03 16:27:21 +08:00
}
2019-11-01 14:51:45 +08:00
/// <summary>
/// Restart gameplay via a parent <see cref="PlayerLoader"/>.
/// <remarks>This can be called from a child screen in order to trigger the restart process.</remarks>
/// </summary>
2022-08-16 12:04:56 +08:00
/// <param name="quickRestart">Whether a quick restart was requested (skipping intro etc.).</param>
2023-10-12 18:06:19 +08:00
/// <returns>Whether this call resulted in a restart.</returns>
2023-10-12 14:42:50 +08:00
public bool Restart ( bool quickRestart = false )
2017-01-29 04:55:42 +08:00
{
2020-12-23 16:39:08 +08:00
if ( ! Configuration . AllowRestart )
2023-10-12 14:42:50 +08:00
return false ;
2020-12-23 16:39:08 +08:00
2022-06-15 16:49:18 +08:00
isRestarting = true ;
2020-10-07 16:40:54 +08:00
// at the point of restarting the track should either already be paused or the volume should be zero.
// stopping here is to ensure music doesn't become audible after exiting back to PlayerLoader.
musicController . Stop ( ) ;
2022-08-16 12:04:56 +08:00
RestartRequested ? . Invoke ( quickRestart ) ;
2019-11-01 14:32:06 +08:00
2023-10-12 14:42:50 +08:00
return PerformExit ( false ) ;
2017-01-28 03:28:39 +08:00
}
2018-04-13 17:19:50 +08:00
2021-06-17 18:13:34 +08:00
/// <summary>
/// This delegate, when set, means the results screen has been queued to appear.
2021-06-18 15:24:07 +08:00
/// The display of the results screen may be delayed by any work being done in <see cref="PrepareScoreForResultsAsync"/>.
2021-06-17 18:13:34 +08:00
/// </summary>
/// <remarks>
2021-06-18 01:02:56 +08:00
/// Once set, this can *only* be cancelled by rewinding, ie. if <see cref="JudgementProcessor.HasCompleted">ScoreProcessor.HasCompleted</see> becomes <see langword="false"/>.
2021-06-17 18:13:34 +08:00
/// Even if the user requests an exit, it will forcefully proceed to the results screen (see special case in <see cref="OnExiting"/>).
/// </remarks>
private ScheduledDelegate resultsDisplayDelegate ;
/// <summary>
/// A task which asynchronously prepares a completed score for display at results.
/// This may include performing net requests or importing the score into the database, generally to ensure things are in a sane state for the play session.
/// </summary>
2021-03-23 14:45:22 +08:00
private Task < ScoreInfo > prepareScoreForDisplayTask ;
2018-04-13 17:19:50 +08:00
2021-05-04 15:43:51 +08:00
/// <summary>
/// Handles changes in player state which may progress the completion of gameplay / this screen's lifetime.
/// </summary>
2023-03-31 01:30:04 +08:00
private void checkScoreCompleted ( )
2016-11-29 22:59:56 +08:00
{
2021-06-18 00:07:54 +08:00
// If this player instance is in the middle of an exit, don't attempt any kind of state update.
2020-03-19 13:10:54 +08:00
if ( ! this . IsCurrentScreen ( ) )
return ;
2023-03-31 01:36:17 +08:00
// Handle cases of arriving at this method when not in a completed state.
// - When a storyboard completion triggered this call earlier than gameplay finishes.
// - When a replay has been rewound before a queued resultsDisplayDelegate has run.
//
// Currently, even if this scenario is hit, prepareAndImportScoreAsync has already been queued (and potentially run).
// In the scenarios above, this is a non-issue, but it still feels a bit convoluted to have to cancel in this method.
// Maybe this can be improved with further refactoring.
2023-03-29 12:30:13 +08:00
if ( ! ScoreProcessor . HasCompleted . Value )
2020-04-19 10:59:56 +08:00
{
2021-06-17 18:13:34 +08:00
resultsDisplayDelegate ? . Cancel ( ) ;
resultsDisplayDelegate = null ;
2022-01-26 00:45:11 +08:00
GameplayState . HasPassed = false ;
2020-04-19 10:59:56 +08:00
ValidForResume = true ;
2021-05-05 09:35:36 +08:00
skipOutroOverlay . Hide ( ) ;
2020-04-19 10:59:56 +08:00
return ;
}
2017-03-17 00:36:30 +08:00
// Only show the completion screen if the player hasn't failed
2024-01-04 15:41:52 +08:00
if ( GameplayState . HasFailed )
2017-03-16 23:30:23 +08:00
return ;
2018-04-13 17:19:50 +08:00
2022-01-26 00:45:11 +08:00
GameplayState . HasPassed = true ;
2021-12-24 13:23:09 +08:00
2021-06-17 18:13:34 +08:00
// Setting this early in the process means that even if something were to go wrong in the order of events following, there
// is no chance that a user could return to the (already completed) Player instance from a child screen.
2017-04-13 10:41:08 +08:00
ValidForResume = false ;
2018-04-13 17:19:50 +08:00
2023-03-29 12:30:13 +08:00
bool storyboardStillRunning = DimmableStoryboard . ContentDisplayed & & ! DimmableStoryboard . HasStoryboardEnded . Value ;
2021-04-19 13:23:21 +08:00
2023-03-29 12:30:13 +08:00
// If the current beatmap has a storyboard, this method will be called again on storyboard completion.
// Alternatively, the user may press the outro skip button, forcing immediate display of the results screen.
if ( storyboardStillRunning )
2021-04-14 12:04:03 +08:00
{
skipOutroOverlay . Show ( ) ;
return ;
}
2021-06-18 14:45:12 +08:00
progressToResults ( true ) ;
2016-11-29 22:59:56 +08:00
}
2018-04-13 17:19:50 +08:00
2021-06-18 14:45:12 +08:00
/// <summary>
/// Queue the results screen for display.
/// </summary>
/// <remarks>
2021-06-18 15:18:20 +08:00
/// A final display will only occur once all work is completed in <see cref="PrepareScoreForResultsAsync"/>. This means that even after calling this method, the results screen will never be shown until <see cref="JudgementProcessor.HasCompleted">ScoreProcessor.HasCompleted</see> becomes <see langword="true"/>.
2021-06-18 14:45:12 +08:00
/// </remarks>
/// <param name="withDelay">Whether a minimum delay (<see cref="RESULTS_DISPLAY_DELAY"/>) should be added before the screen is displayed.</param>
private void progressToResults ( bool withDelay )
{
2023-10-12 18:09:43 +08:00
if ( ! Configuration . ShowResults )
return ;
// Setting this early in the process means that even if something were to go wrong in the order of events following, there
// is no chance that a user could return to the (already completed) Player instance from a child screen.
ValidForResume = false ;
2021-06-18 14:45:12 +08:00
double delay = withDelay ? RESULTS_DISPLAY_DELAY : 0 ;
2023-10-12 18:09:43 +08:00
resultsDisplayDelegate ? . Cancel ( ) ;
2021-06-18 14:45:12 +08:00
resultsDisplayDelegate = new ScheduledDelegate ( ( ) = >
{
2023-02-05 23:35:11 +08:00
if ( prepareScoreForDisplayTask = = null )
{
2023-02-06 14:59:37 +08:00
// Try importing score since the task hasn't been invoked yet.
2023-02-14 15:55:35 +08:00
prepareAndImportScoreAsync ( ) ;
2023-02-05 23:35:11 +08:00
return ;
}
if ( ! prepareScoreForDisplayTask . IsCompleted )
2021-06-18 15:18:20 +08:00
// If the asynchronous preparation has not completed, keep repeating this delegate.
2021-06-18 14:45:12 +08:00
return ;
resultsDisplayDelegate ? . Cancel ( ) ;
2023-02-14 15:55:35 +08:00
if ( prepareScoreForDisplayTask . GetResultSafely ( ) = = null )
{
// If score import did not occur, we do not want to show the results screen.
return ;
}
2021-06-18 14:45:12 +08:00
if ( ! this . IsCurrentScreen ( ) )
// This player instance may already be in the process of exiting.
return ;
2022-01-06 21:54:43 +08:00
this . Push ( CreateResults ( prepareScoreForDisplayTask . GetResultSafely ( ) ) ) ;
2021-06-18 14:45:12 +08:00
} , Time . Current + delay , 50 ) ;
Scheduler . Add ( resultsDisplayDelegate ) ;
2016-11-29 22:59:56 +08:00
}
2020-12-18 17:20:36 +08:00
2023-02-06 14:59:37 +08:00
/// <summary>
2023-02-14 15:55:35 +08:00
/// Asynchronously run score preparation operations (database import, online submission etc.).
2023-02-06 14:59:37 +08:00
/// </summary>
2023-02-14 15:55:35 +08:00
/// <param name="forceImport">Whether the score should be imported even if non-passing (or the current configuration doesn't allow for it).</param>
/// <returns>The final score.</returns>
[ItemCanBeNull]
private Task < ScoreInfo > prepareAndImportScoreAsync ( bool forceImport = false )
2023-02-05 23:35:11 +08:00
{
2023-02-06 14:59:37 +08:00
// Ensure we are not writing to the replay any more, as we are about to consume and store the score.
DrawableRuleset . SetRecordTarget ( null ) ;
2023-02-14 15:55:35 +08:00
if ( prepareScoreForDisplayTask ! = null )
return prepareScoreForDisplayTask ;
2023-02-05 23:35:11 +08:00
// We do not want to import the score in cases where we don't show results
bool canShowResults = Configuration . ShowResults & & ScoreProcessor . HasCompleted . Value & & GameplayState . HasPassed ;
2023-02-14 15:55:35 +08:00
if ( ! canShowResults & & ! forceImport )
return Task . FromResult < ScoreInfo > ( null ) ;
2023-02-06 14:59:37 +08:00
2023-08-15 15:08:11 +08:00
// Clone score before beginning any async processing.
// - Must be run synchronously as the score may potentially be mutated in the background.
// - Must be cloned for the same reason.
Score scoreCopy = Score . DeepClone ( ) ;
2023-02-14 15:55:35 +08:00
return prepareScoreForDisplayTask = Task . Run ( async ( ) = >
2023-02-05 23:35:11 +08:00
{
2023-02-14 15:55:35 +08:00
try
{
await PrepareScoreForResultsAsync ( scoreCopy ) . ConfigureAwait ( false ) ;
}
catch ( Exception ex )
{
Logger . Error ( ex , @"Score preparation failed!" ) ;
}
2023-02-05 23:35:11 +08:00
2023-02-14 15:55:35 +08:00
try
{
await ImportScore ( scoreCopy ) . ConfigureAwait ( false ) ;
}
catch ( Exception ex )
{
Logger . Error ( ex , @"Score import failed!" ) ;
}
return scoreCopy . ScoreInfo ;
} ) ;
2023-02-05 23:35:11 +08:00
}
2021-12-03 15:56:34 +08:00
protected override bool OnScroll ( ScrollEvent e )
{
// During pause, allow global volume adjust regardless of settings.
if ( GameplayClockContainer . IsPaused . Value )
return false ;
// Block global volume adjust if the user has asked for it (special case when holding "Alt").
return mouseWheelDisabled . Value & & ! e . AltPressed ;
}
2019-03-18 10:48:11 +08:00
2022-09-13 15:36:09 +08:00
#region Gameplay leaderboard
protected readonly Bindable < bool > LeaderboardExpandedState = new BindableBool ( ) ;
private void loadLeaderboard ( )
{
HUDOverlay . HoldingForHUD . BindValueChanged ( _ = > updateLeaderboardExpandedState ( ) ) ;
LocalUserPlaying . BindValueChanged ( _ = > updateLeaderboardExpandedState ( ) , true ) ;
2022-09-19 23:06:02 +08:00
var gameplayLeaderboard = CreateGameplayLeaderboard ( ) ;
if ( gameplayLeaderboard ! = null )
2022-09-13 15:36:09 +08:00
{
2022-09-19 23:06:02 +08:00
LoadComponentAsync ( gameplayLeaderboard , leaderboard = >
{
if ( ! LoadedBeatmapSuccessfully )
return ;
2022-09-13 15:36:09 +08:00
2022-09-19 23:06:02 +08:00
leaderboard . Expanded . BindTo ( LeaderboardExpandedState ) ;
2022-09-13 17:12:49 +08:00
2022-09-19 23:06:02 +08:00
AddLeaderboardToHUD ( leaderboard ) ;
} ) ;
}
2022-09-13 15:36:09 +08:00
}
2022-09-19 23:06:02 +08:00
[CanBeNull]
protected virtual GameplayLeaderboard CreateGameplayLeaderboard ( ) = > null ;
2022-09-13 15:36:09 +08:00
2022-09-13 17:23:47 +08:00
protected virtual void AddLeaderboardToHUD ( GameplayLeaderboard leaderboard ) = > HUDOverlay . LeaderboardFlow . Add ( leaderboard ) ;
2022-09-13 15:36:09 +08:00
private void updateLeaderboardExpandedState ( ) = >
LeaderboardExpandedState . Value = ! LocalUserPlaying . Value | | HUDOverlay . HoldingForHUD . Value ;
#endregion
2019-03-18 10:48:11 +08:00
#region Fail Logic
2023-11-23 09:01:59 +08:00
/// <summary>
/// Invoked when gameplay has permanently failed.
/// </summary>
protected virtual void OnFail ( )
{
}
2019-03-18 10:48:11 +08:00
protected FailOverlay FailOverlay { get ; private set ; }
2023-10-07 23:39:30 +08:00
private FailAnimationContainer failAnimationContainer ;
2019-06-04 15:13:16 +08:00
2017-08-05 10:59:58 +08:00
private bool onFail ( )
2017-01-20 15:51:43 +08:00
{
2022-08-25 13:26:42 +08:00
// Failing after the quit sequence has started may cause weird side effects with the fail animation / effects.
if ( GameplayState . HasQuit )
return false ;
2020-05-12 19:08:35 +08:00
if ( ! CheckModsAllowFailure ( ) )
2017-08-05 10:59:58 +08:00
return false ;
2018-04-13 17:19:50 +08:00
2024-01-04 15:41:52 +08:00
if ( Configuration . AllowFailAnimation )
{
Debug . Assert ( ! GameplayState . HasFailed ) ;
Debug . Assert ( ! GameplayState . HasPassed ) ;
Debug . Assert ( ! GameplayState . HasQuit ) ;
2018-10-31 19:03:37 +08:00
2024-01-04 15:41:52 +08:00
GameplayState . HasFailed = true ;
2022-04-26 10:19:19 +08:00
2024-01-04 15:41:52 +08:00
updateGameplayState ( ) ;
2019-03-18 10:48:11 +08:00
2024-01-04 15:41:52 +08:00
// There is a chance that we could be in a paused state as the ruleset's internal clock (see FrameStabilityContainer)
// could process an extra frame after the GameplayClock is stopped.
// In such cases we want the fail state to precede a user triggered pause.
if ( PauseOverlay . State . Value = = Visibility . Visible )
PauseOverlay . Hide ( ) ;
failAnimationContainer . Start ( ) ;
// Failures can be triggered either by a judgement, or by a mod.
//
// For the case of a judgement, due to ordering considerations, ScoreProcessor will not have received
// the final judgement which triggered the failure yet (see DrawableRuleset.NewResult handling above).
//
// A schedule here ensures that any lingering judgements from the current frame are applied before we
// finalise the score as "failed".
Schedule ( ( ) = >
{
ScoreProcessor . FailScore ( Score . ScoreInfo ) ;
OnFail ( ) ;
2019-09-19 16:53:10 +08:00
2024-01-04 15:41:52 +08:00
if ( GameplayState . Mods . OfType < IApplicableFailOverride > ( ) . Any ( m = > m . RestartOnFail ) )
Restart ( true ) ;
} ) ;
}
else
2023-11-23 10:05:52 +08:00
{
ScoreProcessor . FailScore ( Score . ScoreInfo ) ;
2024-01-04 15:41:52 +08:00
}
2018-10-14 23:18:52 +08:00
2017-08-05 10:59:58 +08:00
return true ;
2017-01-20 15:51:43 +08:00
}
2018-04-13 17:19:50 +08:00
2022-07-21 11:01:13 +08:00
/// <summary>
/// Invoked when the fail animation has finished.
/// </summary>
2019-06-04 15:13:16 +08:00
private void onFailComplete ( )
{
GameplayClockContainer . Stop ( ) ;
2019-03-18 10:48:11 +08:00
FailOverlay . Retries = RestartCount ;
FailOverlay . Show ( ) ;
2017-01-20 15:51:43 +08:00
}
2018-04-13 17:19:50 +08:00
2019-03-18 10:48:11 +08:00
#endregion
#region Pause Logic
public bool IsResuming { get ; private set ; }
/// <summary>
/// The amount of gameplay time after which a second pause is allowed.
/// </summary>
private const double pause_cooldown = 1000 ;
protected PauseOverlay PauseOverlay { get ; private set ; }
private double? lastPauseActionTime ;
2021-02-19 16:42:30 +08:00
protected bool PauseCooldownActive = >
2022-08-15 16:06:24 +08:00
lastPauseActionTime . HasValue & & GameplayClockContainer . CurrentTime < lastPauseActionTime + pause_cooldown ;
2021-02-19 14:34:39 +08:00
2021-02-20 12:35:25 +08:00
/// <summary>
/// A set of conditionals which defines whether the current game state and configuration allows for
/// pausing to be attempted via <see cref="Pause"/>. If false, the game should generally exit if a user pause
/// is attempted.
/// </summary>
private bool pausingSupportedByCurrentState = >
2019-03-18 10:48:11 +08:00
// must pass basic screen conditions (beatmap loaded, instance allows pause)
2020-12-23 16:39:08 +08:00
LoadedBeatmapSuccessfully & & Configuration . AllowPause & & ValidForResume
2019-03-18 10:48:11 +08:00
// replays cannot be paused and exit immediately
2019-03-20 14:27:06 +08:00
& & ! DrawableRuleset . HasReplayLoaded . Value
2019-03-18 10:48:11 +08:00
// cannot pause if we are already in a fail state
2022-01-26 00:45:11 +08:00
& & ! GameplayState . HasFailed ;
2019-03-18 10:48:11 +08:00
private bool canResume = >
// cannot resume from a non-paused state
GameplayClockContainer . IsPaused . Value
// cannot resume if we are already in a fail state
2022-01-26 00:45:11 +08:00
& & ! GameplayState . HasFailed
2019-03-18 10:48:11 +08:00
// already resuming
& & ! IsResuming ;
2021-02-22 15:03:27 +08:00
public bool Pause ( )
2019-03-18 10:48:11 +08:00
{
2021-02-22 15:03:27 +08:00
if ( ! pausingSupportedByCurrentState ) return false ;
2021-02-20 12:35:25 +08:00
2021-02-22 15:03:27 +08:00
if ( ! IsResuming & & PauseCooldownActive )
return false ;
2019-03-18 10:48:11 +08:00
2019-10-26 03:57:49 +08:00
if ( IsResuming )
{
DrawableRuleset . CancelResume ( ) ;
2019-11-01 13:43:52 +08:00
IsResuming = false ;
2019-10-26 03:57:49 +08:00
}
2019-03-18 10:48:11 +08:00
GameplayClockContainer . Stop ( ) ;
PauseOverlay . Show ( ) ;
2022-08-15 16:06:24 +08:00
lastPauseActionTime = GameplayClockContainer . CurrentTime ;
2021-02-22 15:03:27 +08:00
return true ;
2019-03-18 10:48:11 +08:00
}
public void Resume ( )
{
if ( ! canResume ) return ;
IsResuming = true ;
PauseOverlay . Hide ( ) ;
2019-03-18 13:40:53 +08:00
2019-04-08 03:32:55 +08:00
// breaks and time-based conditions may allow instant resume.
2020-03-26 14:28:56 +08:00
if ( breakTracker . IsBreakTime . Value )
2019-03-18 13:40:53 +08:00
completeResume ( ) ;
else
2019-03-20 14:27:06 +08:00
DrawableRuleset . RequestResume ( completeResume ) ;
2019-03-18 13:40:53 +08:00
void completeResume ( )
{
GameplayClockContainer . Start ( ) ;
IsResuming = false ;
}
2019-03-18 10:48:11 +08:00
}
#endregion
#region Screen Logic
2022-04-21 23:52:44 +08:00
public override void OnEntering ( ScreenTransitionEvent e )
2016-11-20 00:39:43 +08:00
{
2022-04-21 23:52:44 +08:00
base . OnEntering ( e ) ;
2018-04-13 17:19:50 +08:00
2018-04-20 16:30:27 +08:00
if ( ! LoadedBeatmapSuccessfully )
2017-07-19 18:10:04 +08:00
return ;
2018-04-13 17:19:50 +08:00
2019-01-23 19:52:00 +08:00
Alpha = 0 ;
this
2017-07-16 23:28:20 +08:00
. ScaleTo ( 0.7f )
2017-07-23 02:50:25 +08:00
. ScaleTo ( 1 , 750 , Easing . OutQuint )
2017-07-16 23:28:20 +08:00
. Delay ( 250 )
. FadeIn ( 250 ) ;
2018-04-13 17:19:50 +08:00
2021-01-04 17:32:23 +08:00
ApplyToBackground ( b = >
{
2021-04-13 14:24:35 +08:00
b . IgnoreUserSettings . Value = false ;
2021-01-04 17:32:23 +08:00
b . BlurAmount . Value = 0 ;
2021-06-09 16:07:28 +08:00
b . FadeColour ( Color4 . White , 250 ) ;
2021-01-04 17:32:23 +08:00
// bind component bindables.
2024-01-15 18:21:19 +08:00
( ( IBindable < bool > ) b . IsBreakTime ) . BindTo ( breakTracker . IsBreakTime ) ;
2021-01-04 17:32:23 +08:00
b . StoryboardReplacesBackground . BindTo ( storyboardReplacesBackground ) ;
2021-12-13 13:46:49 +08:00
2023-10-07 23:39:30 +08:00
failAnimationContainer . Background = b ;
2021-01-04 17:32:23 +08:00
} ) ;
2018-04-13 17:19:50 +08:00
2022-04-26 10:19:19 +08:00
HUDOverlay . IsPlaying . BindTo ( localUserPlaying ) ;
2022-10-12 14:11:52 +08:00
ShowingOverlayComponents . BindTo ( HUDOverlay . ShowHud ) ;
2020-03-26 14:28:56 +08:00
DimmableStoryboard . IsBreakTime . BindTo ( breakTracker . IsBreakTime ) ;
2019-12-11 04:06:13 +08:00
2024-02-29 10:39:36 +08:00
storyboardReplacesBackground . Value = GameplayState . Storyboard . ReplacesBackground & & GameplayState . Storyboard . HasDrawable ;
2019-02-18 15:34:11 +08:00
2021-10-07 13:53:36 +08:00
foreach ( var mod in GameplayState . Mods . OfType < IApplicableToPlayer > ( ) )
2019-11-25 15:24:29 +08:00
mod . ApplyToPlayer ( this ) ;
2021-10-07 13:53:36 +08:00
foreach ( var mod in GameplayState . Mods . OfType < IApplicableToHUD > ( ) )
2019-06-29 09:23:59 +08:00
mod . ApplyToHUD ( HUDOverlay ) ;
2020-08-03 03:34:35 +08:00
2021-10-07 13:53:36 +08:00
foreach ( var mod in GameplayState . Mods . OfType < IApplicableToTrack > ( ) )
2022-09-08 16:14:06 +08:00
mod . ApplyToTrack ( GameplayClockContainer . AdjustmentsFromMods ) ;
2020-09-04 03:56:47 +08:00
2020-10-06 20:09:35 +08:00
updateGameplayState ( ) ;
2020-12-24 14:32:55 +08:00
GameplayClockContainer . FadeInFromZero ( 750 , Easing . OutQuint ) ;
2021-08-11 17:16:25 +08:00
2020-12-24 14:32:55 +08:00
StartGameplay ( ) ;
2021-08-11 17:16:25 +08:00
OnGameplayStarted ? . Invoke ( ) ;
2020-12-24 14:32:55 +08:00
}
/// <summary>
/// Called to trigger the starting of the gameplay clock and underlying gameplay.
/// This will be called on entering the player screen once. A derived class may block the first call to this to delay the start of gameplay.
/// </summary>
protected virtual void StartGameplay ( )
{
2022-08-15 16:06:24 +08:00
if ( GameplayClockContainer . IsRunning )
2023-10-10 16:58:11 +08:00
Logger . Error ( new InvalidOperationException ( $"{nameof(StartGameplay)} should not be called when the gameplay clock is already running" ) , "Clock failure" ) ;
2020-12-24 14:32:55 +08:00
2022-08-22 13:11:06 +08:00
GameplayClockContainer . Reset ( startClock : true ) ;
2023-05-22 23:00:53 +08:00
if ( Configuration . AutomaticallySkipIntro )
skipIntroOverlay . SkipWhenReady ( ) ;
2017-02-22 13:14:37 +08:00
}
2018-04-13 17:19:50 +08:00
2022-04-21 23:52:44 +08:00
public override void OnSuspending ( ScreenTransitionEvent e )
2017-02-22 13:14:37 +08:00
{
2021-10-11 13:05:31 +08:00
screenSuspension ? . RemoveAndDisposeImmediately ( ) ;
2020-06-18 22:35:03 +08:00
2017-04-11 18:58:57 +08:00
fadeOut ( ) ;
2022-04-21 23:52:44 +08:00
base . OnSuspending ( e ) ;
2016-10-28 13:14:45 +08:00
}
2018-04-13 17:19:50 +08:00
2022-04-21 23:52:44 +08:00
public override bool OnExiting ( ScreenExitEvent e )
2016-12-17 00:13:24 +08:00
{
2021-10-11 13:05:31 +08:00
screenSuspension ? . RemoveAndDisposeImmediately ( ) ;
2023-02-06 17:22:51 +08:00
// Eagerly clean these up as disposal of child components is asynchronous and may leave sounds playing beyond user expectations.
2023-10-07 23:39:30 +08:00
failAnimationContainer ? . Stop ( ) ;
2023-02-06 18:31:45 +08:00
PauseOverlay ? . StopAllSamples ( ) ;
2020-06-18 22:35:03 +08:00
2023-10-27 19:27:48 +08:00
if ( LoadedBeatmapSuccessfully & & ! GameplayState . HasPassed )
2021-07-19 11:36:13 +08:00
{
2023-10-27 20:39:58 +08:00
Debug . Assert ( resultsDisplayDelegate = = null ) ;
2023-10-27 19:30:51 +08:00
2023-10-27 19:27:48 +08:00
if ( ! GameplayState . HasFailed )
2022-03-09 16:50:05 +08:00
GameplayState . HasQuit = true ;
2023-10-27 19:27:48 +08:00
if ( DrawableRuleset . ReplayScore = = null )
2022-07-21 11:01:13 +08:00
ScoreProcessor . FailScore ( Score . ScoreInfo ) ;
2022-03-09 16:50:05 +08:00
}
2021-05-21 13:09:30 +08:00
2019-11-01 13:11:18 +08:00
// GameplayClockContainer performs seeks / start / stop operations on the beatmap's track.
// as we are no longer the current screen, we cannot guarantee the track is still usable.
2021-04-14 16:47:11 +08:00
( GameplayClockContainer as MasterGameplayClockContainer ) ? . StopUsingBeatmapClock ( ) ;
2019-11-01 13:11:18 +08:00
2020-09-01 17:07:19 +08:00
musicController . ResetTrackAdjustments ( ) ;
2020-09-01 15:55:10 +08:00
2019-03-16 13:20:10 +08:00
fadeOut ( ) ;
2022-07-31 07:29:57 +08:00
2022-04-21 23:52:44 +08:00
return base . OnExiting ( e ) ;
2016-12-17 00:13:24 +08:00
}
2018-04-13 17:19:50 +08:00
2020-12-18 16:47:33 +08:00
/// <summary>
2021-06-02 14:44:04 +08:00
/// Creates the player's <see cref="Scoring.Score"/>.
2020-12-18 16:47:33 +08:00
/// </summary>
2021-10-05 13:48:10 +08:00
/// <param name="beatmap"></param>
2021-06-02 14:44:04 +08:00
/// <returns>The <see cref="Scoring.Score"/>.</returns>
2021-10-05 13:48:10 +08:00
protected virtual Score CreateScore ( IBeatmap beatmap ) = > new Score
2021-06-03 16:47:22 +08:00
{
2023-12-21 17:14:24 +08:00
ScoreInfo = new ScoreInfo
{
User = api . LocalUser . Value ,
2023-12-21 19:56:19 +08:00
ClientVersion = game . Version ,
2023-12-21 17:14:24 +08:00
} ,
2021-06-03 16:47:22 +08:00
} ;
2020-12-18 14:36:24 +08:00
2020-12-18 16:47:33 +08:00
/// <summary>
2021-06-02 14:44:04 +08:00
/// Imports the player's <see cref="Scoring.Score"/> to the local database.
2020-12-18 16:47:33 +08:00
/// </summary>
2021-06-02 14:44:04 +08:00
/// <param name="score">The <see cref="Scoring.Score"/> to import.</param>
2020-12-19 02:32:05 +08:00
/// <returns>The imported score.</returns>
2022-01-25 14:23:51 +08:00
protected virtual Task ImportScore ( Score score )
2020-03-29 21:51:28 +08:00
{
2020-12-18 15:51:59 +08:00
// Replays are already populated and present in the game's database, so should not be re-imported.
2020-03-29 21:51:28 +08:00
if ( DrawableRuleset . ReplayScore ! = null )
2022-01-25 14:23:51 +08:00
return Task . CompletedTask ;
2020-03-29 21:51:28 +08:00
2023-09-14 12:36:07 +08:00
ByteArrayArchiveReader replayReader = null ;
2020-03-29 21:51:28 +08:00
2022-07-07 13:49:22 +08:00
if ( score . ScoreInfo . Ruleset . IsLegacyRuleset ( ) )
2020-03-29 21:51:28 +08:00
{
2022-07-07 13:49:22 +08:00
using ( var stream = new MemoryStream ( ) )
{
new LegacyScoreEncoder ( score , GameplayState . Beatmap ) . Encode ( stream ) ;
2023-09-14 12:36:07 +08:00
replayReader = new ByteArrayArchiveReader ( stream . ToArray ( ) , "replay.osr" ) ;
2022-07-07 13:49:22 +08:00
}
2020-03-29 21:51:28 +08:00
}
2022-01-14 17:22:52 +08:00
// the import process will re-attach managed beatmap/rulesets to this score. we don't want this for now, so create a temporary copy to import.
var importableScore = score . ScoreInfo . DeepClone ( ) ;
2022-01-25 14:23:51 +08:00
var imported = scoreManager . Import ( importableScore , replayReader ) ;
2024-04-23 02:23:38 +08:00
Debug . Assert ( imported ! = null ) ;
2021-05-18 20:17:33 +08:00
2022-01-14 17:22:52 +08:00
imported . PerformRead ( s = >
{
// because of the clone above, it's required that we copy back the post-import hash/ID to use for availability matching.
score . ScoreInfo . Hash = s . Hash ;
score . ScoreInfo . ID = s . ID ;
2023-07-26 14:21:58 +08:00
score . ScoreInfo . Files . AddRange ( s . Files . Detach ( ) ) ;
2022-01-14 17:22:52 +08:00
} ) ;
2022-01-25 14:23:51 +08:00
return Task . CompletedTask ;
2020-03-29 21:51:28 +08:00
}
2020-12-19 02:32:05 +08:00
/// <summary>
2021-06-02 14:44:04 +08:00
/// Prepare the <see cref="Scoring.Score"/> for display at results.
2020-12-19 02:32:05 +08:00
/// </summary>
2021-06-02 14:44:04 +08:00
/// <param name="score">The <see cref="Scoring.Score"/> to prepare.</param>
2021-03-23 14:45:22 +08:00
/// <returns>A task that prepares the provided score. On completion, the score is assumed to be ready for display.</returns>
2021-07-19 18:28:35 +08:00
protected virtual Task PrepareScoreForResultsAsync ( Score score ) = > Task . CompletedTask ;
2020-12-19 02:32:05 +08:00
2020-12-18 16:47:33 +08:00
/// <summary>
/// Creates the <see cref="ResultsScreen"/> for a <see cref="ScoreInfo"/>.
/// </summary>
/// <param name="score">The <see cref="ScoreInfo"/> to be displayed in the results screen.</param>
/// <returns>The <see cref="ResultsScreen"/>.</returns>
2024-02-23 02:15:02 +08:00
protected virtual ResultsScreen CreateResults ( ScoreInfo score ) = > new SoloResultsScreen ( score )
2022-12-27 06:25:54 +08:00
{
2024-02-23 02:15:02 +08:00
AllowRetry = true ,
ShowUserStatistics = true ,
2022-12-27 06:25:54 +08:00
} ;
2020-12-18 14:36:24 +08:00
2018-08-02 18:08:23 +08:00
private void fadeOut ( bool instant = false )
2017-09-14 20:28:53 +08:00
{
2018-08-02 18:08:23 +08:00
float fadeOutDuration = instant ? 0 : 250 ;
2019-01-23 19:52:00 +08:00
this . FadeOut ( fadeOutDuration ) ;
2018-04-13 17:19:50 +08:00
2023-10-12 14:42:50 +08:00
if ( this . IsCurrentScreen ( ) )
{
2024-01-15 18:21:19 +08:00
ApplyToBackground ( b = >
{
b . IgnoreUserSettings . Value = true ;
2024-01-22 15:37:56 +08:00
// May be null if the load never completed.
if ( breakTracker ! = null )
{
b . IsBreakTime . UnbindFrom ( breakTracker . IsBreakTime ) ;
b . IsBreakTime . Value = false ;
}
2024-01-15 18:21:19 +08:00
} ) ;
2024-01-22 15:37:56 +08:00
2023-10-12 14:42:50 +08:00
storyboardReplacesBackground . Value = false ;
}
2017-09-14 20:28:53 +08:00
}
2018-04-13 17:19:50 +08:00
2019-03-18 10:48:11 +08:00
#endregion
2020-10-14 18:39:48 +08:00
IBindable < bool > ISamplePlaybackDisabler . SamplePlaybackDisabled = > samplePlaybackDisabled ;
2021-08-17 15:13:45 +08:00
IBindable < bool > ILocalUserPlayInfo . IsPlaying = > LocalUserPlaying ;
2016-09-29 19:13:58 +08:00
}
2017-04-11 23:09:45 +08:00
}