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
using System ;
2019-04-09 12:33:16 +08:00
using System.Collections.Generic ;
2020-03-24 13:13:46 +08:00
using System.IO ;
2018-04-13 17:19:50 +08:00
using System.Linq ;
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 ;
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 ;
using osu.Game.Beatmaps ;
using osu.Game.Configuration ;
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 ;
2020-03-23 18:31:43 +08:00
using osu.Game.Replays ;
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 ;
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 ;
2018-04-13 17:19:50 +08:00
namespace osu.Game.Screens.Play
{
2019-11-01 14:32:06 +08:00
[Cached]
2019-03-05 17:06:24 +08:00
public class Player : ScreenWithBeatmapBackground
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 ;
2019-06-25 15:55:49 +08:00
public override bool AllowBackButton = > false ; // handled by HoldForMenuButton
2018-04-13 17:19:50 +08:00
2019-06-12 15:33:15 +08:00
protected override UserActivity InitialActivity = > new UserActivity . SoloGame ( 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
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).
public override bool AllowRateAdjustments = > false ;
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
2018-04-13 17:19:50 +08:00
public Action RestartRequested ;
public bool HasFailed { get ; private set ; }
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
2020-10-07 15:22:39 +08:00
protected 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 ;
2018-11-29 13:56:29 +08:00
[Resolved]
private ScoreManager scoreManager { get ; set ; }
2018-04-13 17:19:50 +08:00
2019-08-26 11:21:49 +08:00
private RulesetInfo rulesetInfo ;
2018-04-13 17:19:50 +08:00
2019-08-26 11:21:49 +08:00
private Ruleset ruleset ;
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
2018-04-13 17:19:50 +08:00
private SampleChannel sampleRestart ;
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 ;
2020-05-13 03:12:48 +08:00
private SkipOverlay skipOverlay ;
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-04-09 12:33:16 +08:00
[Cached]
2019-04-17 15:11:59 +08:00
[Cached(Type = typeof(IBindable<IReadOnlyList<Mod>>))]
2019-04-25 16:36:17 +08:00
protected new readonly Bindable < IReadOnlyList < Mod > > Mods = new Bindable < IReadOnlyList < Mod > > ( Array . Empty < Mod > ( ) ) ;
2019-04-09 12:33:16 +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>
2020-05-12 19:08:35 +08:00
protected virtual bool CheckModsAllowFailure ( ) = > Mods . Value . OfType < IApplicableFailOverride > ( ) . All ( m = > m . PerformFail ( ) ) ;
2019-09-19 03:49:28 +08:00
2019-03-26 15:53:44 +08:00
private readonly bool allowPause ;
private readonly bool showResults ;
/// <summary>
/// Create a new player instance.
/// </summary>
/// <param name="allowPause">Whether pausing should be allowed. If not allowed, attempting to pause will quit.</param>
/// <param name="showResults">Whether results screen should be pushed on completion.</param>
public Player ( bool allowPause = true , bool showResults = true )
{
this . allowPause = allowPause ;
this . showResults = showResults ;
}
2018-04-13 17:19:50 +08:00
2020-02-14 11:30:11 +08:00
private GameplayBeatmap gameplayBeatmap ;
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 ( ) ;
PrepareReplay ( ) ;
}
private Replay recordingReplay ;
/// <summary>
/// Run any recording / playback setup for replays.
/// </summary>
protected virtual void PrepareReplay ( )
{
DrawableRuleset . SetRecordTarget ( recordingReplay = new Replay ( ) ) ;
}
2020-10-07 13:46:58 +08:00
[BackgroundDependencyLoader(true)]
private void load ( AudioManager audio , OsuConfigManager config , OsuGame game )
2018-04-13 17:19:50 +08:00
{
2019-04-17 15:11:59 +08:00
Mods . Value = base . Mods . Value . Select ( m = > m . CreateCopy ( ) ) . ToArray ( ) ;
2019-04-09 12:33:16 +08:00
2019-12-12 14:58:11 +08:00
if ( Beatmap . Value is DummyWorkingBeatmap )
return ;
IBeatmap playableBeatmap = loadPlayableBeatmap ( ) ;
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 )
2020-10-07 15:22:39 +08:00
LocalUserPlaying . BindTo ( game . LocalUserPlaying ) ;
2020-10-06 20:09:35 +08:00
2019-12-12 14:58:11 +08:00
DrawableRuleset = ruleset . CreateDrawableRulesetWith ( playableBeatmap , Mods . Value ) ;
2019-12-24 16:01:17 +08:00
ScoreProcessor = ruleset . CreateScoreProcessor ( ) ;
ScoreProcessor . ApplyBeatmap ( playableBeatmap ) ;
2019-04-25 18:56:57 +08:00
ScoreProcessor . Mods . BindTo ( Mods ) ;
2018-04-13 17:19:50 +08:00
2019-12-27 15:14:49 +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
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-08-17 21:36:03 +08:00
InternalChild = GameplayClockContainer = new GameplayClockContainer ( Beatmap . Value , DrawableRuleset . GameplayStartTime ) ;
2018-04-13 17:19:50 +08:00
2020-02-14 11:30:11 +08:00
AddInternal ( gameplayBeatmap = new GameplayBeatmap ( playableBeatmap ) ) ;
2020-06-18 22:35:03 +08:00
AddInternal ( screenSuspension = new ScreenSuspensionHandler ( GameplayClockContainer ) ) ;
2020-02-14 11:30:11 +08:00
dependencies . CacheAs ( gameplayBeatmap ) ;
2020-09-29 13:09:51 +08:00
var beatmapSkinProvider = new BeatmapSkinProvidingContainer ( Beatmap . Value . Skin ) ;
// the beatmapSkinProvider is used as the fallback source here to allow the ruleset-specific skin implementation
// full access to all skin sources.
var rulesetSkinProvider = new SkinProvidingContainer ( ruleset . CreateLegacySkinProvider ( beatmapSkinProvider , playableBeatmap ) ) ;
// 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.
GameplayClockContainer . Add ( beatmapSkinProvider . WithChild ( rulesetSkinProvider ) ) ;
rulesetSkinProvider . AddRange ( new [ ]
{
// underlay and gameplay should have access the to skinning sources.
createUnderlayComponents ( ) ,
createGameplayComponents ( Beatmap . Value , playableBeatmap )
} ) ;
// add the overlay components as a separate step as they proxy some elements from the above underlay/gameplay components.
GameplayClockContainer . 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-05-13 03:12:48 +08:00
skipOverlay . Hide ( ) ;
2020-05-07 14:52:36 +08:00
}
2020-10-06 20:09:35 +08:00
DrawableRuleset . IsPaused . BindValueChanged ( _ = > updateGameplayState ( ) ) ;
DrawableRuleset . HasReplayLoaded . BindValueChanged ( _ = > updateGameplayState ( ) ) ;
breakTracker . IsBreakTime . BindValueChanged ( _ = > updateGameplayState ( ) ) ;
2020-08-16 23:18:40 +08:00
2020-03-06 17:00:17 +08:00
DrawableRuleset . HasReplayLoaded . BindValueChanged ( _ = > updatePauseOnFocusLostState ( ) , true ) ;
2018-04-13 17:19:50 +08:00
2019-08-27 17:27:21 +08:00
// bind clock into components that require it
DrawableRuleset . IsPaused . BindTo ( GameplayClockContainer . IsPaused ) ;
2018-04-13 17:19:50 +08:00
2019-12-19 19:03:14 +08:00
DrawableRuleset . OnNewResult + = r = >
{
HealthProcessor . ApplyResult ( r ) ;
ScoreProcessor . ApplyResult ( r ) ;
2020-05-03 22:55:44 +08:00
gameplayBeatmap . ApplyResult ( r ) ;
2019-12-19 19:03:14 +08:00
} ;
DrawableRuleset . OnRevertResult + = r = >
{
HealthProcessor . RevertResult ( r ) ;
ScoreProcessor . RevertResult ( r ) ;
} ;
2019-12-11 16:25:06 +08:00
2019-12-19 19:03:14 +08:00
// Bind the judgement processors to ourselves
2020-04-19 10:58:22 +08:00
ScoreProcessor . HasCompleted . ValueChanged + = updateCompletionState ;
2019-12-19 19:03:14 +08:00
HealthProcessor . Failed + = onFail ;
2018-04-13 17:19:50 +08:00
2019-08-27 17:27:21 +08:00
foreach ( var mod in Mods . Value . OfType < IApplicableToScoreProcessor > ( ) )
mod . ApplyToScoreProcessor ( ScoreProcessor ) ;
2019-12-19 19:03:14 +08:00
foreach ( var mod in Mods . Value . OfType < IApplicableToHealthProcessor > ( ) )
mod . ApplyToHealthProcessor ( HealthProcessor ) ;
2020-10-11 20:46:55 +08:00
IsBreakTime . BindTo ( breakTracker . IsBreakTime ) ;
2020-03-26 14:28:56 +08:00
breakTracker . IsBreakTime . BindValueChanged ( onBreakTimeChanged , true ) ;
2019-08-27 17:27:21 +08:00
}
2018-04-13 17:19:50 +08:00
2020-09-29 13:09:51 +08:00
private Drawable createUnderlayComponents ( ) = >
DimmableStoryboard = new DimmableStoryboard ( Beatmap . Value . Storyboard ) { RelativeSizeAxes = Axes . Both } ;
2018-04-13 17:19:50 +08:00
2020-09-29 13:09:51 +08:00
private Drawable createGameplayComponents ( WorkingBeatmap working , IBeatmap playableBeatmap ) = > 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 ,
breakTracker = new BreakTracker ( DrawableRuleset . GameplayStartTime , ScoreProcessor )
{
Breaks = working . Beatmap . Breaks
}
} ) ,
2019-08-27 17:27:21 +08:00
new ComboEffects ( ScoreProcessor )
2020-09-29 13:09:51 +08:00
}
} ;
2018-04-13 17:19:50 +08:00
2020-09-29 13:09:51 +08:00
private Drawable createOverlayComponents ( WorkingBeatmap working ) = > new Container
2019-08-27 17:27:21 +08:00
{
2020-09-29 13:09:51 +08:00
RelativeSizeAxes = Axes . Both ,
Children = new [ ]
2018-04-13 17:19:50 +08:00
{
2020-05-20 01:47:01 +08:00
DimmableStoryboard . OverlayLayerContainer . CreateProxy ( ) ,
2020-03-28 04:19:49 +08:00
BreakOverlay = new BreakOverlay ( working . Beatmap . BeatmapInfo . LetterboxInBreaks , ScoreProcessor )
{
Clock = DrawableRuleset . FrameStableClock ,
ProcessCustomClock = false ,
Breaks = working . Beatmap . Breaks
} ,
2019-03-18 10:48:11 +08:00
// display the cursor above some HUD elements.
2019-03-20 14:27:06 +08:00
DrawableRuleset . Cursor ? . CreateProxy ( ) ? ? new Container ( ) ,
2019-09-02 10:20:50 +08:00
DrawableRuleset . ResumeOverlay ? . CreateProxy ( ) ? ? new Container ( ) ,
2019-12-19 19:03:14 +08:00
HUDOverlay = new HUDOverlay ( ScoreProcessor , HealthProcessor , DrawableRuleset , Mods . Value )
2019-03-18 10:48:11 +08:00
{
2019-05-10 14:51:12 +08:00
HoldToQuit =
2018-04-13 17:19:50 +08:00
{
2019-05-10 14:51:12 +08:00
Action = performUserRequestedExit ,
IsPaused = { BindTarget = GameplayClockContainer . IsPaused }
} ,
2019-03-18 10:48:11 +08:00
PlayerSettingsOverlay = { PlaybackSettings = { UserPlaybackRate = { BindTarget = GameplayClockContainer . UserPlaybackRate } } } ,
2020-03-06 17:00:17 +08:00
KeyCounter =
{
AlwaysVisible = { BindTarget = DrawableRuleset . HasReplayLoaded } ,
IsCounting = false
} ,
2019-03-18 10:48:11 +08:00
RequestSeek = GameplayClockContainer . Seek ,
Anchor = Anchor . Centre ,
Origin = Anchor . Centre
} ,
2020-05-13 03:12:48 +08:00
skipOverlay = new SkipOverlay ( DrawableRuleset . GameplayStartTime )
2019-03-18 10:48:11 +08:00
{
2019-11-21 17:57:19 +08:00
RequestSkip = GameplayClockContainer . Skip
2019-03-18 10:48:11 +08:00
} ,
FailOverlay = new FailOverlay
{
2019-03-18 13:57:06 +08:00
OnRetry = Restart ,
2019-03-18 10:48:11 +08:00
OnQuit = performUserRequestedExit ,
2018-04-13 17:19:50 +08:00
} ,
2019-03-18 10:48:11 +08:00
PauseOverlay = new PauseOverlay
2018-04-13 17:19:50 +08:00
{
2019-03-18 10:48:11 +08:00
OnResume = Resume ,
Retries = RestartCount ,
2018-04-13 17:19:50 +08:00
OnRetry = Restart ,
2018-12-13 15:17:24 +08:00
OnQuit = performUserRequestedExit ,
2018-04-13 17:19:50 +08:00
} ,
new HotkeyRetryOverlay
{
Action = ( ) = >
{
2019-01-23 19:52:00 +08:00
if ( ! this . IsCurrentScreen ( ) ) return ;
2018-04-13 17:19:50 +08:00
2018-08-02 18:08:23 +08:00
fadeOut ( true ) ;
2018-04-13 17:19:50 +08:00
Restart ( ) ;
} ,
2019-06-04 15:13:16 +08:00
} ,
2019-06-24 17:15:27 +08:00
new HotkeyExitOverlay
{
Action = ( ) = >
{
if ( ! this . IsCurrentScreen ( ) ) return ;
fadeOut ( true ) ;
2019-06-25 22:15:58 +08:00
performImmediateExit ( ) ;
2019-06-24 17:15:27 +08:00
} ,
} ,
2020-03-26 11:50:18 +08:00
failAnimation = new FailAnimation ( DrawableRuleset ) { OnComplete = onFailComplete , } ,
2020-09-29 13:09:51 +08:00
}
} ;
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-03-06 17:00:17 +08:00
updatePauseOnFocusLostState ( ) ;
HUDOverlay . KeyCounter . IsCounting = ! 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
{
2020-10-06 20:09:35 +08:00
bool inGameplay = ! DrawableRuleset . HasReplayLoaded . Value & & ! DrawableRuleset . IsPaused . Value & & ! breakTracker . IsBreakTime . Value ;
OverlayActivationMode . Value = inGameplay ? OverlayActivation . Disabled : OverlayActivation . UserTriggered ;
2020-10-07 15:22:39 +08:00
LocalUserPlaying . Value = inGameplay ;
2020-08-04 03:25:45 +08:00
}
2020-03-06 17:00:17 +08:00
private void updatePauseOnFocusLostState ( ) = >
2019-12-11 14:45:50 +08:00
HUDOverlay . HoldToQuit . PauseOnFocusLost = PauseOnFocusLost
2020-03-06 17:00:17 +08:00
& & ! DrawableRuleset . HasReplayLoaded . Value
2020-03-26 14:28:56 +08:00
& & ! breakTracker . IsBreakTime . Value ;
2019-12-11 14:45:50 +08:00
2019-12-12 14:58:11 +08:00
private IBeatmap loadPlayableBeatmap ( )
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
2019-12-12 14:58:11 +08:00
rulesetInfo = Ruleset . Value ? ? Beatmap . Value . BeatmapInfo . Ruleset ;
2019-08-26 11:21:49 +08:00
ruleset = rulesetInfo . CreateInstance ( ) ;
2019-03-06 19:30:14 +08:00
try
{
2019-12-12 14:58:11 +08:00
playable = Beatmap . Value . GetPlayableBeatmap ( ruleset . RulesetInfo , Mods . Value ) ;
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 ;
2019-08-26 11:21:49 +08:00
ruleset = rulesetInfo . CreateInstance ( ) ;
2019-12-12 14:58:11 +08:00
playable = Beatmap . Value . GetPlayableBeatmap ( rulesetInfo , Mods . Value ) ;
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 ;
}
}
catch ( Exception e )
{
2020-09-23 15:30:20 +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
2019-06-25 22:15:58 +08:00
private void performImmediateExit ( )
2018-12-13 15:17:24 +08:00
{
2019-06-24 17:15:27 +08:00
// if a restart has been requested, cancel any pending completion (user has shown intent to restart).
2019-08-06 22:05:12 +08:00
completionProgressDelegate ? . Cancel ( ) ;
2018-04-13 17:19:50 +08:00
2019-06-24 17:15:27 +08:00
ValidForResume = false ;
2018-04-13 17:19:50 +08:00
2019-06-25 22:15:58 +08:00
performUserRequestedExit ( ) ;
2018-04-13 17:19:50 +08:00
}
2019-06-25 22:15:58 +08:00
private void performUserRequestedExit ( )
2018-04-13 17:19:50 +08:00
{
2019-06-25 22:15:58 +08:00
if ( ! this . IsCurrentScreen ( ) ) return ;
2018-04-13 17:19:50 +08:00
2019-10-04 11:41:53 +08:00
if ( ValidForResume & & HasFailed & & ! FailOverlay . IsPresent )
2019-10-04 11:23:42 +08:00
{
failAnimation . FinishTransforms ( true ) ;
return ;
}
2019-10-03 02:16:31 +08:00
if ( canPause )
Pause ( ) ;
else
this . Exit ( ) ;
2018-04-13 17:19:50 +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>
2018-04-13 17:19:50 +08:00
public void Restart ( )
{
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 ( ) ;
RestartRequested ? . Invoke ( ) ;
2019-11-01 14:32:06 +08:00
if ( this . IsCurrentScreen ( ) )
performImmediateExit ( ) ;
2019-11-01 14:51:10 +08:00
else
this . MakeCurrent ( ) ;
2018-04-13 17:19:50 +08:00
}
2019-08-06 22:05:12 +08:00
private ScheduledDelegate completionProgressDelegate ;
2018-04-13 17:19:50 +08:00
2020-04-19 10:58:22 +08:00
private void updateCompletionState ( ValueChangedEvent < bool > completionState )
2018-04-13 17:19:50 +08:00
{
2020-03-19 13:10:54 +08:00
// screen may be in the exiting transition phase.
if ( ! this . IsCurrentScreen ( ) )
return ;
2020-04-19 10:59:56 +08:00
if ( ! completionState . NewValue )
{
completionProgressDelegate ? . Cancel ( ) ;
completionProgressDelegate = null ;
ValidForResume = true ;
return ;
}
2020-04-21 10:51:20 +08:00
if ( completionProgressDelegate ! = null )
throw new InvalidOperationException ( $"{nameof(updateCompletionState)} was fired more than once" ) ;
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 ;
ValidForResume = false ;
2019-03-26 15:53:44 +08:00
if ( ! showResults ) return ;
2018-04-13 17:19:50 +08:00
2020-04-20 11:42:33 +08:00
using ( BeginDelayedSequence ( RESULTS_DISPLAY_DELAY ) )
2020-04-21 10:51:20 +08:00
completionProgressDelegate = Schedule ( GotoRanking ) ;
2018-04-13 17:19:50 +08:00
}
2018-11-30 17:32:08 +08:00
protected virtual ScoreInfo CreateScore ( )
2018-11-29 12:22:45 +08:00
{
2020-03-17 15:42:55 +08:00
var score = new ScoreInfo
2018-11-29 12:22:45 +08:00
{
2018-11-30 17:31:54 +08:00
Beatmap = Beatmap . Value . BeatmapInfo ,
2019-08-26 11:21:49 +08:00
Ruleset = rulesetInfo ,
2019-04-10 11:03:57 +08:00
Mods = Mods . Value . ToArray ( ) ,
2018-11-29 12:22:45 +08:00
} ;
2020-03-17 15:42:55 +08:00
if ( DrawableRuleset . ReplayScore ! = null )
score . User = DrawableRuleset . ReplayScore . ScoreInfo ? . User ? ? new GuestUser ( ) ;
else
score . User = api . LocalUser . Value ;
2018-11-29 12:22:45 +08:00
ScoreProcessor . PopulateScore ( score ) ;
return score ;
}
2019-03-18 10:48:11 +08:00
protected override bool OnScroll ( ScrollEvent e ) = > mouseWheelDisabled . Value & & ! GameplayClockContainer . IsPaused . Value ;
2020-05-26 16:00:41 +08:00
protected virtual ResultsScreen CreateResults ( ScoreInfo score ) = > new SoloResultsScreen ( score ) ;
2019-03-18 10:48:11 +08:00
#region Fail Logic
protected FailOverlay FailOverlay { get ; private set ; }
2019-06-04 15:13:16 +08:00
private FailAnimation failAnimation ;
2018-04-13 17:19:50 +08:00
private bool onFail ( )
{
2020-05-12 19:08:35 +08:00
if ( ! CheckModsAllowFailure ( ) )
2018-04-13 17:19:50 +08:00
return false ;
2018-10-31 19:03:37 +08:00
HasFailed = true ;
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 ( ) ;
2019-09-19 00:45:59 +08:00
failAnimation . Start ( ) ;
2019-09-19 16:53:10 +08:00
if ( Mods . Value . OfType < IApplicableFailOverride > ( ) . Any ( m = > m . RestartOnFail ) )
2018-10-14 23:18:52 +08:00
Restart ( ) ;
2018-04-13 17:19:50 +08:00
return true ;
}
2019-06-04 15:13:16 +08:00
// Called back when the transform finishes
private void onFailComplete ( )
{
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 ;
private bool canPause = >
// must pass basic screen conditions (beatmap loaded, instance allows pause)
2019-03-26 15:53:44 +08:00
LoadedBeatmapSuccessfully & & 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
& & ! HasFailed
2019-03-22 13:42:51 +08:00
// cannot pause if already paused (or in a cooldown state) unless we are in a resuming state.
2019-11-24 15:44:35 +08:00
& & ( IsResuming | | ( GameplayClockContainer . IsPaused . Value = = false & & ! pauseCooldownActive ) ) ;
2019-03-18 13:40:53 +08:00
private bool pauseCooldownActive = >
lastPauseActionTime . HasValue & & GameplayClockContainer . GameplayClock . CurrentTime < lastPauseActionTime + pause_cooldown ;
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
& & ! HasFailed
// already resuming
& & ! IsResuming ;
public void Pause ( )
{
if ( ! canPause ) return ;
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 ( ) ;
lastPauseActionTime = GameplayClockContainer . GameplayClock . CurrentTime ;
}
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
2019-01-23 19:52:00 +08:00
public override void OnEntering ( IScreen last )
2018-04-13 17:19:50 +08:00
{
base . OnEntering ( last ) ;
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 ) ;
2019-11-25 15:24:29 +08:00
Background . EnableUserDim . Value = true ;
Background . BlurAmount . Value = 0 ;
2018-04-13 17:19:50 +08:00
2019-12-11 04:06:13 +08:00
// bind component bindables.
2020-03-26 14:28:56 +08:00
Background . IsBreakTime . BindTo ( breakTracker . IsBreakTime ) ;
DimmableStoryboard . IsBreakTime . BindTo ( breakTracker . IsBreakTime ) ;
2019-12-11 04:06:13 +08:00
2019-02-28 19:01:15 +08:00
Background . StoryboardReplacesBackground . BindTo ( storyboardReplacesBackground ) ;
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
2019-03-17 23:46:15 +08:00
GameplayClockContainer . Restart ( ) ;
2019-03-18 10:48:11 +08:00
GameplayClockContainer . FadeInFromZero ( 750 , Easing . OutQuint ) ;
2018-04-13 17:19:50 +08:00
2019-11-25 15:24:29 +08:00
foreach ( var mod in Mods . Value . OfType < IApplicableToPlayer > ( ) )
mod . ApplyToPlayer ( this ) ;
2019-06-29 09:23:59 +08:00
foreach ( var mod in Mods . Value . OfType < IApplicableToHUD > ( ) )
mod . ApplyToHUD ( HUDOverlay ) ;
2020-08-03 03:34:35 +08:00
2020-09-01 17:07:19 +08:00
// Our mods are local copies of the global mods so they need to be re-applied to the track.
// This is done through the music controller (for now), because resetting speed adjustments on the beatmap track also removes adjustments provided by DrawableTrack.
// Todo: In the future, player will receive in a track and will probably not have to worry about this...
musicController . ResetTrackAdjustments ( ) ;
2020-09-01 15:55:10 +08:00
foreach ( var mod in Mods . Value . OfType < IApplicableToTrack > ( ) )
2020-09-01 17:07:19 +08:00
mod . ApplyToTrack ( musicController . CurrentTrack ) ;
2020-09-04 03:56:47 +08:00
2020-10-06 20:09:35 +08:00
updateGameplayState ( ) ;
2018-04-13 17:19:50 +08:00
}
2019-01-23 19:52:00 +08:00
public override void OnSuspending ( IScreen next )
2018-04-13 17:19:50 +08:00
{
2020-06-18 22:35:03 +08:00
screenSuspension ? . Expire ( ) ;
2018-04-13 17:19:50 +08:00
fadeOut ( ) ;
base . OnSuspending ( next ) ;
}
2019-01-23 19:52:00 +08:00
public override bool OnExiting ( IScreen next )
2018-04-13 17:19:50 +08:00
{
2020-06-18 22:35:03 +08:00
screenSuspension ? . Expire ( ) ;
2019-08-06 22:05:12 +08:00
if ( completionProgressDelegate ! = null & & ! completionProgressDelegate . Cancelled & & ! completionProgressDelegate . Completed )
2018-07-28 06:34:51 +08:00
{
2019-08-06 22:05:12 +08:00
// proceed to result screen if beatmap already finished playing
2020-03-19 13:10:54 +08:00
completionProgressDelegate . RunTask ( ) ;
2018-07-28 06:34:51 +08:00
return true ;
}
2019-09-13 14:41:53 +08:00
// ValidForResume is false when restarting
if ( ValidForResume )
2019-06-04 15:13:16 +08:00
{
2019-09-13 14:41:53 +08:00
if ( pauseCooldownActive & & ! GameplayClockContainer . IsPaused . Value )
// still want to block if we are within the cooldown period and not already paused.
return true ;
2018-04-13 17:19:50 +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.
2019-12-23 18:13:36 +08:00
GameplayClockContainer ? . 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 ( ) ;
return base . OnExiting ( next ) ;
2018-04-13 17:19:50 +08:00
}
2020-03-29 21:51:28 +08:00
protected virtual void GotoRanking ( )
{
if ( DrawableRuleset . ReplayScore ! = null )
{
// if a replay is present, we likely don't want to import into the local database.
this . Push ( CreateResults ( CreateScore ( ) ) ) ;
return ;
}
LegacyByteArrayReader replayReader = null ;
var score = new Score { ScoreInfo = CreateScore ( ) } ;
if ( recordingReplay ? . Frames . Count > 0 )
{
score . Replay = recordingReplay ;
using ( var stream = new MemoryStream ( ) )
{
new LegacyScoreEncoder ( score , gameplayBeatmap . PlayableBeatmap ) . Encode ( stream ) ;
replayReader = new LegacyByteArrayReader ( stream . ToArray ( ) , "replay.osr" ) ;
}
}
scoreManager . Import ( score . ScoreInfo , replayReader )
. ContinueWith ( imported = > Schedule ( ( ) = >
{
// screen may be in the exiting transition phase.
if ( this . IsCurrentScreen ( ) )
this . Push ( CreateResults ( imported . Result ) ) ;
} ) ) ;
}
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
2019-02-20 15:53:57 +08:00
Background . EnableUserDim . Value = false ;
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
2018-04-13 17:19:50 +08:00
}
}