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-04-13 17:19:50 +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-04-13 17:19:50 +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 ;
2018-04-13 17:19:50 +08:00
using osu.Framework.Allocation ;
using osu.Framework.Audio ;
using osu.Framework.Audio.Sample ;
2019-02-21 18:04:31 +08:00
using osu.Framework.Bindables ;
2022-01-03 16:31:12 +08:00
using osu.Framework.Extensions ;
2018-04-13 17:19:50 +08:00
using osu.Framework.Graphics ;
using osu.Framework.Graphics.Containers ;
2018-10-02 11:02:47 +08:00
using osu.Framework.Input.Events ;
2018-04-13 17:19:50 +08:00
using osu.Framework.Logging ;
using osu.Framework.Screens ;
using osu.Framework.Threading ;
2022-05-07 22:17:23 +08:00
using osu.Game.Audio ;
2018-04-13 17:19:50 +08:00
using osu.Game.Beatmaps ;
using osu.Game.Configuration ;
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 ;
2018-04-13 17:19:50 +08:00
using osu.Game.Online.API ;
2018-06-06 14:10:09 +08:00
using osu.Game.Overlays ;
2018-04-13 17:19:50 +08:00
using osu.Game.Rulesets ;
using osu.Game.Rulesets.Mods ;
using osu.Game.Rulesets.Scoring ;
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-04-13 17:19:50 +08:00
using osu.Game.Screens.Ranking ;
using osu.Game.Skinning ;
2019-04-13 04:54:35 +08:00
using osu.Game.Users ;
2021-06-09 16:07:28 +08:00
using osuTK.Graphics ;
2018-04-13 17:19:50 +08:00
namespace osu.Game.Screens.Play
{
2019-11-01 14:32:06 +08:00
[Cached]
2022-11-24 13:32:20 +08:00
public abstract partial class Player : ScreenWithBeatmapBackground , ISamplePlaybackDisabler , ILocalUserPlayInfo
2018-04-13 17:19:50 +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).
2021-09-16 15:08:09 +08:00
public override bool? AllowTrackAdjustments = > 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-04-13 17:19:50 +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
2018-04-13 17:19:50 +08:00
public int RestartCount ;
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 > ( ) ;
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
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
2021-01-19 16:11:40 +08:00
private Sample sampleRestart ;
2018-04-13 17:19:50 +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)]
2022-06-09 13:03:21 +08:00
private void load ( AudioManager audio , OsuConfigManager config , OsuGameBase game , CancellationToken cancellationToken )
2018-04-13 17:19:50 +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-04-13 17:19:50 +08:00
return ;
2019-05-28 16:06:01 +08:00
sampleRestart = audio . Samples . Get ( @"Gameplay/restart" ) ;
2018-04-13 17:19:50 +08:00
mouseWheelDisabled = config . GetBindable < bool > ( OsuSetting . MouseDisableWheel ) ;
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 ) ;
2021-10-02 01:22:23 +08:00
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 ) ;
2018-06-29 15:49:11 +08:00
if ( ! ScoreProcessor . Mode . Disabled )
config . BindWith ( OsuSetting . ScoreDisplayMode , ScoreProcessor . Mode ) ;
2018-04-13 17:19:50 +08:00
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
2022-05-30 18:14:03 +08:00
dependencies . CacheAs ( GameplayState = new GameplayState ( playableBeatmap , ruleset , gameplayMods , Score , ScoreProcessor ) ) ;
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
{
2021-10-15 18:35:08 +08:00
failAnimationLayer = new FailAnimation ( DrawableRuleset )
{
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
}
} ,
FailOverlay = new FailOverlay
{
2023-02-14 15:55:35 +08:00
SaveReplay = async ( ) = > await prepareAndImportScoreAsync ( true ) . ConfigureAwait ( false ) ,
2022-08-16 12:04:56 +08:00
OnRetry = ( ) = > Restart ( ) ,
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 ;
fadeOut ( true ) ;
PerformExit ( false ) ;
} ,
} ,
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 )
{
rulesetSkinProvider . Add ( new HotkeyRetryOverlay
{
Action = ( ) = >
{
if ( ! this . IsCurrentScreen ( ) ) return ;
fadeOut ( true ) ;
2022-08-16 12:04:56 +08:00
Restart ( true ) ;
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.
2021-10-16 01:29:45 +08:00
failAnimationLayer . 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-08-16 13:27:02 +08:00
if ( Configuration . AutomaticallySkipIntro )
skipIntroOverlay . SkipWhenReady ( ) ;
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 ( ) = >
2022-03-03 01:33:46 +08:00
DimmableStoryboard = new DimmableStoryboard ( Beatmap . Value . 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
2018-04-13 17:19:50 +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
} ,
KeyCounter =
{
2023-02-22 22:58:27 +08:00
IsCounting =
{
Value = false
} ,
2021-10-15 18:35:08 +08:00
AlwaysVisible = { BindTarget = DrawableRuleset . HasReplayLoaded } ,
} ,
Anchor = Anchor . Centre ,
Origin = Anchor . Centre
} ,
skipIntroOverlay = new SkipOverlay ( DrawableRuleset . GameplayStartTime )
{
RequestSkip = performUserRequestedSkip
2021-10-15 18:14:59 +08:00
} ,
2021-10-15 18:35:08 +08:00
skipOutroOverlay = new SkipOverlay ( Beatmap . Value . 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
2021-04-14 16:47:11 +08:00
if ( GameplayClockContainer is MasterGameplayClockContainer master )
HUDOverlay . PlayerSettingsOverlay . PlaybackSettings . UserPlaybackRate . BindTarget = master . UserPlaybackRate ;
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-02-22 22:58:27 +08:00
HUDOverlay . KeyCounter . 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 )
2018-04-13 17:19:50 +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 ) ;
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
{
Logger . Log ( "Beatmap contains no hit objects!" , level : LogLevel . Error ) ;
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 ;
2018-04-13 17:19:50 +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>
2021-02-09 16:14:16 +08:00
protected void PerformExit ( bool showDialogFirst )
2018-12-13 15:17:24 +08:00
{
2021-06-17 17:10:59 +08:00
// there is a chance that an exit request occurs after the transition to results has already started.
// even in such a case, the user has shown intent, so forcefully return to this screen (to proceed with the upwards exit process).
2021-05-24 14:14:55 +08:00
if ( ! this . IsCurrentScreen ( ) )
2021-02-09 15:24:29 +08:00
{
ValidForResume = false ;
2021-05-24 14:14:55 +08:00
// in the potential case that this instance has already been exited, this is required to avoid a crash.
if ( this . GetChildScreen ( ) ! = null )
this . MakeCurrent ( ) ;
2021-02-15 13:03:41 +08:00
return ;
2021-02-09 15:24:29 +08:00
}
2018-04-13 17:19:50 +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
{
2021-10-15 18:35:08 +08:00
failAnimationLayer . FinishTransforms ( true ) ;
2021-02-09 15:24:29 +08:00
return ;
}
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 ( ) ;
return ;
}
2021-02-09 15:24:29 +08:00
}
2022-10-02 20:30:06 +08:00
// if an exit has been requested, cancel any pending completion (the user has shown intention to exit).
resultsDisplayDelegate ? . Cancel ( ) ;
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
2021-06-17 17:10:59 +08:00
// 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.
2021-02-09 15:24:29 +08:00
this . Exit ( ) ;
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>
public void Restart ( bool quickRestart = false )
2018-04-13 17:19:50 +08:00
{
2020-12-23 16:39:08 +08:00
if ( ! Configuration . AllowRestart )
return ;
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 ( ) ;
2018-04-13 17:19:50 +08:00
sampleRestart ? . Play ( ) ;
2022-08-16 12:04:56 +08:00
RestartRequested ? . Invoke ( quickRestart ) ;
2019-11-01 14:32:06 +08:00
2021-02-09 16:14:16 +08:00
PerformExit ( false ) ;
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 ( )
2018-04-13 17:19:50 +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 ;
}
2018-04-13 17:19:50 +08:00
// Only show the completion screen if the player hasn't failed
2020-04-21 10:51:20 +08:00
if ( HealthProcessor . HasFailed )
2018-04-13 17:19:50 +08:00
return ;
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.
2018-04-13 17:19:50 +08:00
ValidForResume = false ;
2021-06-17 18:13:34 +08:00
if ( ! Configuration . ShowResults )
2021-04-19 13:23:21 +08:00
return ;
2020-12-18 17:20:36 +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 ) ;
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 )
{
2022-10-02 20:37:56 +08:00
resultsDisplayDelegate ? . Cancel ( ) ;
2021-06-18 14:45:12 +08:00
double delay = withDelay ? RESULTS_DISPLAY_DELAY : 0 ;
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 ;
2023-03-29 13:11:56 +08:00
Debug . Assert ( ScoreProcessor . Rank . Value ! = ScoreRank . F ) ;
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 ) ;
2018-04-13 17:19:50 +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-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
var scoreCopy = Score . DeepClone ( ) ;
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
protected FailOverlay FailOverlay { get ; private set ; }
2021-10-15 18:35:08 +08:00
private FailAnimation failAnimationLayer ;
2019-06-04 15:13:16 +08:00
2018-04-13 17:19:50 +08:00
private bool onFail ( )
{
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 ( ) )
2018-04-13 17:19:50 +08:00
return false ;
2022-08-25 13:26:42 +08:00
Debug . Assert ( ! GameplayState . HasFailed ) ;
Debug . Assert ( ! GameplayState . HasPassed ) ;
Debug . Assert ( ! GameplayState . HasQuit ) ;
2022-01-26 00:45:11 +08:00
GameplayState . HasFailed = true ;
2018-10-31 19:03:37 +08:00
2022-04-26 10:19:19 +08:00
updateGameplayState ( ) ;
2019-03-18 10:48:11 +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.
2019-06-11 13:28:52 +08:00
if ( PauseOverlay . State . Value = = Visibility . Visible )
2019-03-18 10:48:11 +08:00
PauseOverlay . Hide ( ) ;
2021-10-15 18:35:08 +08:00
failAnimationLayer . Start ( ) ;
2019-09-19 16:53:10 +08:00
2021-10-07 13:53:36 +08:00
if ( GameplayState . Mods . OfType < IApplicableFailOverride > ( ) . Any ( m = > m . RestartOnFail ) )
2022-08-16 12:04:56 +08:00
Restart ( true ) ;
2018-10-14 23:18:52 +08:00
2018-04-13 17:19:50 +08:00
return true ;
}
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 ( )
{
2022-07-21 11:01:13 +08:00
// fail completion is a good point to mark a score as failed,
// since the last judgement that caused the fail only applies to score processor after onFail.
// todo: this should probably be handled better.
ScoreProcessor . FailScore ( Score . ScoreInfo ) ;
2019-06-04 15:13:16 +08:00
GameplayClockContainer . Stop ( ) ;
2019-03-18 10:48:11 +08:00
FailOverlay . Retries = RestartCount ;
FailOverlay . Show ( ) ;
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 )
2018-04-13 17:19:50 +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 )
2018-04-13 17:19:50 +08:00
return ;
2019-01-23 19:52:00 +08:00
Alpha = 0 ;
this
2018-04-13 17:19:50 +08:00
. ScaleTo ( 0.7f )
. ScaleTo ( 1 , 750 , Easing . OutQuint )
. Delay ( 250 )
. FadeIn ( 250 ) ;
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.
b . IsBreakTime . BindTo ( breakTracker . IsBreakTime ) ;
b . StoryboardReplacesBackground . BindTo ( storyboardReplacesBackground ) ;
2021-12-13 13:46:49 +08:00
failAnimationLayer . 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
2019-07-12 10:50:06 +08:00
DimmableStoryboard . StoryboardReplacesBackground . BindTo ( storyboardReplacesBackground ) ;
2018-04-13 17:19:50 +08:00
2019-02-25 12:15:37 +08:00
storyboardReplacesBackground . Value = Beatmap . Value . Storyboard . ReplacesBackground & & Beatmap . Value . 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 )
2020-12-24 14:32:55 +08:00
throw new InvalidOperationException ( $"{nameof(StartGameplay)} should not be called when the gameplay clock is already running" ) ;
2022-08-22 13:11:06 +08:00
GameplayClockContainer . Reset ( startClock : true ) ;
2018-04-13 17:19:50 +08:00
}
2022-04-21 23:52:44 +08:00
public override void OnSuspending ( ScreenTransitionEvent e )
2018-04-13 17:19:50 +08:00
{
2021-10-11 13:05:31 +08:00
screenSuspension ? . RemoveAndDisposeImmediately ( ) ;
2020-06-18 22:35:03 +08:00
2018-04-13 17:19:50 +08:00
fadeOut ( ) ;
2022-04-21 23:52:44 +08:00
base . OnSuspending ( e ) ;
2018-04-13 17:19:50 +08:00
}
2022-04-21 23:52:44 +08:00
public override bool OnExiting ( ScreenExitEvent e )
2018-04-13 17:19:50 +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-01-22 16:27:06 +08:00
failAnimationLayer ? . Stop ( ) ;
2023-02-06 18:31:45 +08:00
PauseOverlay ? . StopAllSamples ( ) ;
2020-06-18 22:35:03 +08:00
2022-03-09 16:50:05 +08:00
if ( LoadedBeatmapSuccessfully )
2021-07-19 11:36:13 +08:00
{
2022-03-09 16:50:05 +08:00
if ( ! GameplayState . HasPassed & & ! GameplayState . HasFailed )
GameplayState . HasQuit = true ;
// if arriving here and the results screen preparation task hasn't run, it's safe to say the user has not completed the beatmap.
2023-03-16 16:14:20 +08:00
if ( prepareScoreForDisplayTask = = null & & 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 ) ;
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
{
ScoreInfo = new ScoreInfo { User = api . LocalUser . Value } ,
} ;
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
2022-07-07 13:49:22 +08:00
LegacyByteArrayReader 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 ) ;
replayReader = new LegacyByteArrayReader ( stream . ToArray ( ) , "replay.osr" ) ;
}
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 ( ) ;
2021-05-18 20:17:33 +08:00
// For the time being, online ID responses are not really useful for anything.
// In addition, the IDs provided via new (lazer) endpoints are based on a different autoincrement from legacy (stable) scores.
//
// Until we better define the server-side logic behind this, let's not store the online ID to avoid potential unique constraint
// conflicts across various systems (ie. solo and multiplayer).
2022-01-14 17:22:52 +08:00
importableScore . OnlineID = - 1 ;
2022-01-14 17:03:06 +08:00
2022-01-25 14:23:51 +08:00
var imported = scoreManager . Import ( importableScore , replayReader ) ;
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 ;
} ) ;
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>
2022-12-27 06:25:54 +08:00
protected virtual ResultsScreen CreateResults ( ScoreInfo score ) = > new SoloResultsScreen ( score , true )
{
ShowUserStatistics = true
} ;
2020-12-18 14:36:24 +08:00
2018-08-02 18:08:23 +08:00
private void fadeOut ( bool instant = false )
2018-04-13 17:19:50 +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
2021-04-13 14:24:35 +08:00
ApplyToBackground ( b = > b . IgnoreUserSettings . Value = true ) ;
2019-02-25 12:15:37 +08:00
storyboardReplacesBackground . Value = false ;
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 ;
2018-04-13 17:19:50 +08:00
}
}