// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Input.Events; using osu.Framework.Logging; using osu.Framework.Screens; using osu.Framework.Threading; using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Graphics.Containers; using osu.Game.IO.Archives; using osu.Game.Online.API; using osu.Game.Online.Spectator; using osu.Game.Overlays; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; using osu.Game.Scoring; using osu.Game.Scoring.Legacy; using osu.Game.Screens.Ranking; using osu.Game.Skinning; using osu.Game.Users; using osuTK.Graphics; namespace osu.Game.Screens.Play { [Cached] [Cached(typeof(ISamplePlaybackDisabler))] public abstract class Player : ScreenWithBeatmapBackground, ISamplePlaybackDisabler { /// /// The delay upon completion of the beatmap before displaying the results screen. /// public const double RESULTS_DISPLAY_DELAY = 1000.0; public override bool AllowBackButton => false; // handled by HoldForMenuButton protected override UserActivity InitialActivity => new UserActivity.SoloGame(Beatmap.Value.BeatmapInfo, Ruleset.Value); public override float BackgroundParallaxAmount => 0.1f; public override bool HideOverlaysOnEnter => true; protected override OverlayActivation InitialOverlayActivationMode => OverlayActivation.UserTriggered; // We are managing our own adjustments (see OnEntering/OnExiting). public override bool AllowRateAdjustments => false; private readonly IBindable gameActive = new Bindable(true); private readonly Bindable samplePlaybackDisabled = new Bindable(); /// /// Whether gameplay should pause when the game window focus is lost. /// protected virtual bool PauseOnFocusLost => true; public Action RestartRequested; public bool HasFailed { get; private set; } private Bindable mouseWheelDisabled; private readonly Bindable storyboardReplacesBackground = new Bindable(); protected readonly Bindable LocalUserPlaying = new Bindable(); public int RestartCount; [Resolved] private ScoreManager scoreManager { get; set; } [Resolved] private IAPIProvider api { get; set; } [Resolved] private MusicController musicController { get; set; } [Resolved] private SpectatorClient spectatorClient { get; set; } protected Ruleset GameplayRuleset { get; private set; } protected GameplayBeatmap GameplayBeatmap { get; private set; } private Sample sampleRestart; public BreakOverlay BreakOverlay; /// /// Whether the gameplay is currently in a break. /// public readonly IBindable IsBreakTime = new BindableBool(); private BreakTracker breakTracker; private SkipOverlay skipIntroOverlay; private SkipOverlay skipOutroOverlay; protected ScoreProcessor ScoreProcessor { get; private set; } protected HealthProcessor HealthProcessor { get; private set; } protected DrawableRuleset DrawableRuleset { get; private set; } protected HUDOverlay HUDOverlay { get; private set; } public bool LoadedBeatmapSuccessfully => DrawableRuleset?.Objects.Any() == true; protected GameplayClockContainer GameplayClockContainer { get; private set; } public DimmableStoryboard DimmableStoryboard { get; private set; } [Cached] [Cached(Type = typeof(IBindable>))] protected new readonly Bindable> Mods = new Bindable>(Array.Empty()); /// /// Whether failing should be allowed. /// By default, this checks whether all selected mods allow failing. /// protected virtual bool CheckModsAllowFailure() => Mods.Value.OfType().All(m => m.PerformFail()); public readonly PlayerConfiguration Configuration; protected Score Score { get; private set; } /// /// Create a new player instance. /// protected Player(PlayerConfiguration configuration = null) { Configuration = configuration ?? new PlayerConfiguration(); } private ScreenSuspensionHandler screenSuspension; private DependencyContainer dependencies; protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) => dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); protected override void LoadComplete() { base.LoadComplete(); if (!LoadedBeatmapSuccessfully) return; Score = CreateScore(); // ensure the score is in a consistent state with the current player. Score.ScoreInfo.Beatmap = Beatmap.Value.BeatmapInfo; Score.ScoreInfo.Ruleset = GameplayRuleset.RulesetInfo; Score.ScoreInfo.Mods = Mods.Value.ToArray(); PrepareReplay(); ScoreProcessor.NewJudgement += result => ScoreProcessor.PopulateScore(Score.ScoreInfo); gameActive.BindValueChanged(_ => updatePauseOnFocusLostState(), true); } /// /// Run any recording / playback setup for replays. /// protected virtual void PrepareReplay() { DrawableRuleset.SetRecordTarget(Score); } [BackgroundDependencyLoader(true)] private void load(AudioManager audio, OsuConfigManager config, OsuGameBase game) { Mods.Value = base.Mods.Value.Select(m => m.CreateCopy()).ToArray(); if (Beatmap.Value is DummyWorkingBeatmap) return; IBeatmap playableBeatmap = loadPlayableBeatmap(); if (playableBeatmap == null) return; sampleRestart = audio.Samples.Get(@"Gameplay/restart"); mouseWheelDisabled = config.GetBindable(OsuSetting.MouseDisableWheel); if (game != null) gameActive.BindTo(game.IsActive); if (game is OsuGame osuGame) LocalUserPlaying.BindTo(osuGame.LocalUserPlaying); DrawableRuleset = GameplayRuleset.CreateDrawableRulesetWith(playableBeatmap, Mods.Value); dependencies.CacheAs(DrawableRuleset); ScoreProcessor = GameplayRuleset.CreateScoreProcessor(); ScoreProcessor.ApplyBeatmap(playableBeatmap); ScoreProcessor.Mods.BindTo(Mods); dependencies.CacheAs(ScoreProcessor); HealthProcessor = GameplayRuleset.CreateHealthProcessor(playableBeatmap.HitObjects[0].StartTime); HealthProcessor.ApplyBeatmap(playableBeatmap); dependencies.CacheAs(HealthProcessor); if (!ScoreProcessor.Mode.Disabled) config.BindWith(OsuSetting.ScoreDisplayMode, ScoreProcessor.Mode); InternalChild = GameplayClockContainer = CreateGameplayClockContainer(Beatmap.Value, DrawableRuleset.GameplayStartTime); AddInternal(GameplayBeatmap = new GameplayBeatmap(playableBeatmap)); AddInternal(screenSuspension = new ScreenSuspensionHandler(GameplayClockContainer)); dependencies.CacheAs(GameplayBeatmap); var rulesetSkinProvider = new RulesetSkinProvidingContainer(GameplayRuleset, playableBeatmap, Beatmap.Value.Skin); // 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(rulesetSkinProvider); rulesetSkinProvider.AddRange(new[] { // underlay and gameplay should have access to the 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. // 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. rulesetSkinProvider.Add(createOverlayComponents(Beatmap.Value)); if (!DrawableRuleset.AllowGameplayOverlays) { HUDOverlay.ShowHud.Value = false; HUDOverlay.ShowHud.Disabled = true; BreakOverlay.Hide(); } DrawableRuleset.FrameStableClock.WaitingOnFrames.BindValueChanged(waiting => { if (waiting.NewValue) GameplayClockContainer.Stop(); else GameplayClockContainer.Start(); }); DrawableRuleset.IsPaused.BindValueChanged(paused => { updateGameplayState(); updateSampleDisabledState(); }); DrawableRuleset.FrameStableClock.IsCatchingUp.BindValueChanged(_ => updateSampleDisabledState()); DrawableRuleset.HasReplayLoaded.BindValueChanged(_ => updateGameplayState()); // bind clock into components that require it DrawableRuleset.IsPaused.BindTo(GameplayClockContainer.IsPaused); DrawableRuleset.NewResult += r => { HealthProcessor.ApplyResult(r); ScoreProcessor.ApplyResult(r); GameplayBeatmap.ApplyResult(r); }; DrawableRuleset.RevertResult += r => { HealthProcessor.RevertResult(r); ScoreProcessor.RevertResult(r); }; DimmableStoryboard.HasStoryboardEnded.ValueChanged += storyboardEnded => { if (storyboardEnded.NewValue) progressToResults(true); }; // Bind the judgement processors to ourselves ScoreProcessor.HasCompleted.BindValueChanged(scoreCompletionChanged); HealthProcessor.Failed += onFail; foreach (var mod in Mods.Value.OfType()) mod.ApplyToScoreProcessor(ScoreProcessor); foreach (var mod in Mods.Value.OfType()) mod.ApplyToHealthProcessor(HealthProcessor); IsBreakTime.BindTo(breakTracker.IsBreakTime); IsBreakTime.BindValueChanged(onBreakTimeChanged, true); } protected virtual GameplayClockContainer CreateGameplayClockContainer(WorkingBeatmap beatmap, double gameplayStart) => new MasterGameplayClockContainer(beatmap, gameplayStart); private Drawable createUnderlayComponents() => DimmableStoryboard = new DimmableStoryboard(Beatmap.Value.Storyboard) { RelativeSizeAxes = Axes.Both }; private Drawable createGameplayComponents(WorkingBeatmap working, IBeatmap playableBeatmap) => new ScalingContainer(ScalingMode.Gameplay) { Children = new Drawable[] { DrawableRuleset.With(r => r.FrameStableComponents.Children = new Drawable[] { ScoreProcessor, HealthProcessor, new ComboEffects(ScoreProcessor), breakTracker = new BreakTracker(DrawableRuleset.GameplayStartTime, ScoreProcessor) { Breaks = working.Beatmap.Breaks } }), } }; private Drawable createOverlayComponents(WorkingBeatmap working) { var container = new Container { RelativeSizeAxes = Axes.Both, Children = new[] { DimmableStoryboard.OverlayLayerContainer.CreateProxy(), BreakOverlay = new BreakOverlay(working.Beatmap.BeatmapInfo.LetterboxInBreaks, ScoreProcessor) { 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(), HUDOverlay = new HUDOverlay(DrawableRuleset, Mods.Value) { HoldToQuit = { Action = () => PerformExit(true), IsPaused = { BindTarget = GameplayClockContainer.IsPaused } }, KeyCounter = { AlwaysVisible = { BindTarget = DrawableRuleset.HasReplayLoaded }, IsCounting = false }, Anchor = Anchor.Centre, Origin = Anchor.Centre }, skipIntroOverlay = new SkipOverlay(DrawableRuleset.GameplayStartTime) { RequestSkip = performUserRequestedSkip }, skipOutroOverlay = new SkipOverlay(Beatmap.Value.Storyboard.LatestEventTime ?? 0) { RequestSkip = () => progressToResults(false), Alpha = 0 }, FailOverlay = new FailOverlay { OnRetry = Restart, OnQuit = () => PerformExit(true), }, PauseOverlay = new PauseOverlay { OnResume = Resume, Retries = RestartCount, OnRetry = Restart, OnQuit = () => PerformExit(true), }, new HotkeyExitOverlay { Action = () => { if (!this.IsCurrentScreen()) return; fadeOut(true); PerformExit(false); }, }, failAnimation = new FailAnimation(DrawableRuleset) { OnComplete = onFailComplete, }, } }; if (!Configuration.AllowSkipping || !DrawableRuleset.AllowGameplayOverlays) { skipIntroOverlay.Expire(); skipOutroOverlay.Expire(); } if (GameplayClockContainer is MasterGameplayClockContainer master) HUDOverlay.PlayerSettingsOverlay.PlaybackSettings.UserPlaybackRate.BindTarget = master.UserPlaybackRate; if (Configuration.AllowRestart) { container.Add(new HotkeyRetryOverlay { Action = () => { if (!this.IsCurrentScreen()) return; fadeOut(true); Restart(); }, }); } return container; } private void onBreakTimeChanged(ValueChangedEvent isBreakTime) { updateGameplayState(); updatePauseOnFocusLostState(); HUDOverlay.KeyCounter.IsCounting = !isBreakTime.NewValue; } private void updateGameplayState() { bool inGameplay = !DrawableRuleset.HasReplayLoaded.Value && !DrawableRuleset.IsPaused.Value && !breakTracker.IsBreakTime.Value; OverlayActivationMode.Value = inGameplay ? OverlayActivation.Disabled : OverlayActivation.UserTriggered; LocalUserPlaying.Value = inGameplay; } private void updateSampleDisabledState() { samplePlaybackDisabled.Value = DrawableRuleset.FrameStableClock.IsCatchingUp.Value || GameplayClockContainer.GameplayClock.IsPaused.Value; } private void updatePauseOnFocusLostState() { if (!PauseOnFocusLost || !pausingSupportedByCurrentState || breakTracker.IsBreakTime.Value) return; if (gameActive.Value == false) { bool paused = Pause(); // if the initial pause could not be satisfied, the pause cooldown may be active. // reschedule the pause attempt until it can be achieved. if (!paused) Scheduler.AddOnce(updatePauseOnFocusLostState); } } private IBeatmap loadPlayableBeatmap() { IBeatmap playable; try { if (Beatmap.Value.Beatmap == null) throw new InvalidOperationException("Beatmap was not loaded"); var rulesetInfo = Ruleset.Value ?? Beatmap.Value.BeatmapInfo.Ruleset; GameplayRuleset = rulesetInfo.CreateInstance(); try { playable = Beatmap.Value.GetPlayableBeatmap(GameplayRuleset.RulesetInfo, Mods.Value); } catch (BeatmapInvalidForRulesetException) { // 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; GameplayRuleset = rulesetInfo.CreateInstance(); playable = Beatmap.Value.GetPlayableBeatmap(rulesetInfo, Mods.Value); } if (playable.HitObjects.Count == 0) { Logger.Log("Beatmap contains no hit objects!", level: LogLevel.Error); return null; } } catch (Exception e) { Logger.Error(e, "Could not load beatmap successfully!"); //couldn't load, hard abort! return null; } return playable; } /// /// Attempts to complete a user request to exit gameplay. /// /// /// /// This should only be called in response to a user interaction. Exiting is not guaranteed. /// This will interrupt any pending progression to the results screen, even if the transition has begun. /// /// /// /// Whether the pause or fail dialog should be shown before performing an exit. /// If and a dialog is not yet displayed, the exit will be blocked and the relevant dialog will display instead. /// protected void PerformExit(bool showDialogFirst) { // if an exit has been requested, cancel any pending completion (the user has shown intention to exit). resultsDisplayDelegate?.Cancel(); // 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). if (!this.IsCurrentScreen()) { ValidForResume = false; // in the potential case that this instance has already been exited, this is required to avoid a crash. if (this.GetChildScreen() != null) this.MakeCurrent(); return; } bool pauseOrFailDialogVisible = PauseOverlay.State.Value == Visibility.Visible || FailOverlay.State.Value == Visibility.Visible; if (showDialogFirst && !pauseOrFailDialogVisible) { // if the fail animation is currently in progress, accelerate it (it will show the pause dialog on completion). if (ValidForResume && HasFailed) { failAnimation.FinishTransforms(true); return; } // even if this call has requested a dialog, there is a chance the current player mode doesn't support pausing. 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; } } // 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(); } private void performUserRequestedSkip() { // user requested skip // disable sample playback to stop currently playing samples and perform skip samplePlaybackDisabled.Value = true; (GameplayClockContainer as MasterGameplayClockContainer)?.Skip(); // return samplePlaybackDisabled.Value to what is defined by the beatmap's current state updateSampleDisabledState(); } /// /// Seek to a specific time in gameplay. /// /// The destination time to seek to. public void Seek(double time) => GameplayClockContainer.Seek(time); private ScheduledDelegate frameStablePlaybackResetDelegate; /// /// Seeks to a specific time in gameplay, bypassing frame stability. /// /// /// Intermediate hitobject judgements may not be applied or reverted correctly during this seek. /// /// The destination time to seek to. internal void NonFrameStableSeek(double time) { if (frameStablePlaybackResetDelegate?.Cancelled == false && !frameStablePlaybackResetDelegate.Completed) frameStablePlaybackResetDelegate.RunTask(); bool wasFrameStable = DrawableRuleset.FrameStablePlayback; DrawableRuleset.FrameStablePlayback = false; Seek(time); // Delay resetting frame-stable playback for one frame to give the FrameStabilityContainer a chance to seek. frameStablePlaybackResetDelegate = ScheduleAfterChildren(() => DrawableRuleset.FrameStablePlayback = wasFrameStable); } /// /// Restart gameplay via a parent . /// This can be called from a child screen in order to trigger the restart process. /// public void Restart() { if (!Configuration.AllowRestart) return; // 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(); sampleRestart?.Play(); RestartRequested?.Invoke(); PerformExit(false); } /// /// This delegate, when set, means the results screen has been queued to appear. /// The display of the results screen may be delayed by any work being done in . /// /// /// Once set, this can *only* be cancelled by rewinding, ie. if ScoreProcessor.HasCompleted becomes . /// Even if the user requests an exit, it will forcefully proceed to the results screen (see special case in ). /// private ScheduledDelegate resultsDisplayDelegate; /// /// 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. /// private Task prepareScoreForDisplayTask; /// /// Handles changes in player state which may progress the completion of gameplay / this screen's lifetime. /// /// Thrown if this method is called more than once without changing state. private void scoreCompletionChanged(ValueChangedEvent completed) { // If this player instance is in the middle of an exit, don't attempt any kind of state update. if (!this.IsCurrentScreen()) return; // Special case to handle rewinding post-completion. This is the only way already queued forward progress can be cancelled. // TODO: Investigate whether this can be moved to a RewindablePlayer subclass or similar. // Currently, even if this scenario is hit, prepareScoreForDisplay has already been queued (and potentially run). // In scenarios where rewinding is possible (replay, spectating) this is a non-issue as no submission/import work is done, // but it still doesn't feel right that this exists here. if (!completed.NewValue) { resultsDisplayDelegate?.Cancel(); resultsDisplayDelegate = null; ValidForResume = true; skipOutroOverlay.Hide(); return; } // Only show the completion screen if the player hasn't failed if (HealthProcessor.HasFailed) 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; // Ensure we are not writing to the replay any more, as we are about to consume and store the score. DrawableRuleset.SetRecordTarget(null); if (!Configuration.ShowResults) return; prepareScoreForDisplayTask ??= Task.Run(prepareScoreForResults); bool storyboardHasOutro = DimmableStoryboard.ContentDisplayed && !DimmableStoryboard.HasStoryboardEnded.Value; if (storyboardHasOutro) { // if the current beatmap has a storyboard, the progression to results will be handled by the storyboard ending // or the user pressing the skip outro button. skipOutroOverlay.Show(); return; } progressToResults(true); } /// /// Asynchronously run score preparation operations (database import, online submission etc.). /// /// The final score. private async Task prepareScoreForResults() { try { await PrepareScoreForResultsAsync(Score).ConfigureAwait(false); } catch (Exception ex) { Logger.Error(ex, @"Score preparation failed!"); } try { await ImportScore(Score).ConfigureAwait(false); } catch (Exception ex) { Logger.Error(ex, @"Score import failed!"); } return Score.ScoreInfo; } /// /// Queue the results screen for display. /// /// /// A final display will only occur once all work is completed in . This means that even after calling this method, the results screen will never be shown until ScoreProcessor.HasCompleted becomes . /// /// Calling this method multiple times will have no effect. /// /// Whether a minimum delay () should be added before the screen is displayed. private void progressToResults(bool withDelay) { if (resultsDisplayDelegate != null) // Note that if progressToResults is called one withDelay=true and then withDelay=false, this no-delay timing will not be // accounted for. shouldn't be a huge concern (a user pressing the skip button after a results progression has already been queued // may take x00 more milliseconds than expected in the very rare edge case). // // If required we can handle this more correctly by rescheduling here. return; double delay = withDelay ? RESULTS_DISPLAY_DELAY : 0; resultsDisplayDelegate = new ScheduledDelegate(() => { if (prepareScoreForDisplayTask?.IsCompleted != true) // If the asynchronous preparation has not completed, keep repeating this delegate. return; resultsDisplayDelegate?.Cancel(); if (!this.IsCurrentScreen()) // This player instance may already be in the process of exiting. return; this.Push(CreateResults(prepareScoreForDisplayTask.Result)); }, Time.Current + delay, 50); Scheduler.Add(resultsDisplayDelegate); } protected override bool OnScroll(ScrollEvent e) => mouseWheelDisabled.Value && !GameplayClockContainer.IsPaused.Value; #region Fail Logic protected FailOverlay FailOverlay { get; private set; } private FailAnimation failAnimation; private bool onFail() { if (!CheckModsAllowFailure()) return false; HasFailed = true; Score.ScoreInfo.Passed = false; // 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(); failAnimation.Start(); if (Mods.Value.OfType().Any(m => m.RestartOnFail)) Restart(); return true; } // Called back when the transform finishes private void onFailComplete() { GameplayClockContainer.Stop(); FailOverlay.Retries = RestartCount; FailOverlay.Show(); } #endregion #region Pause Logic public bool IsResuming { get; private set; } /// /// The amount of gameplay time after which a second pause is allowed. /// private const double pause_cooldown = 1000; protected PauseOverlay PauseOverlay { get; private set; } private double? lastPauseActionTime; protected bool PauseCooldownActive => lastPauseActionTime.HasValue && GameplayClockContainer.GameplayClock.CurrentTime < lastPauseActionTime + pause_cooldown; /// /// A set of conditionals which defines whether the current game state and configuration allows for /// pausing to be attempted via . If false, the game should generally exit if a user pause /// is attempted. /// private bool pausingSupportedByCurrentState => // must pass basic screen conditions (beatmap loaded, instance allows pause) LoadedBeatmapSuccessfully && Configuration.AllowPause && ValidForResume // replays cannot be paused and exit immediately && !DrawableRuleset.HasReplayLoaded.Value // cannot pause if we are already in a fail state && !HasFailed; 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 bool Pause() { if (!pausingSupportedByCurrentState) return false; if (!IsResuming && PauseCooldownActive) return false; if (IsResuming) { DrawableRuleset.CancelResume(); IsResuming = false; } GameplayClockContainer.Stop(); PauseOverlay.Show(); lastPauseActionTime = GameplayClockContainer.GameplayClock.CurrentTime; return true; } public void Resume() { if (!canResume) return; IsResuming = true; PauseOverlay.Hide(); // breaks and time-based conditions may allow instant resume. if (breakTracker.IsBreakTime.Value) completeResume(); else DrawableRuleset.RequestResume(completeResume); void completeResume() { GameplayClockContainer.Start(); IsResuming = false; } } #endregion #region Screen Logic public override void OnEntering(IScreen last) { base.OnEntering(last); if (!LoadedBeatmapSuccessfully) return; Alpha = 0; this .ScaleTo(0.7f) .ScaleTo(1, 750, Easing.OutQuint) .Delay(250) .FadeIn(250); ApplyToBackground(b => { b.IgnoreUserSettings.Value = false; b.BlurAmount.Value = 0; b.FadeColour(Color4.White, 250); // bind component bindables. b.IsBreakTime.BindTo(breakTracker.IsBreakTime); b.StoryboardReplacesBackground.BindTo(storyboardReplacesBackground); }); HUDOverlay.IsBreakTime.BindTo(breakTracker.IsBreakTime); DimmableStoryboard.IsBreakTime.BindTo(breakTracker.IsBreakTime); DimmableStoryboard.StoryboardReplacesBackground.BindTo(storyboardReplacesBackground); storyboardReplacesBackground.Value = Beatmap.Value.Storyboard.ReplacesBackground && Beatmap.Value.Storyboard.HasDrawable; foreach (var mod in Mods.Value.OfType()) mod.ApplyToPlayer(this); foreach (var mod in Mods.Value.OfType()) mod.ApplyToHUD(HUDOverlay); // 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(); foreach (var mod in Mods.Value.OfType()) mod.ApplyToTrack(musicController.CurrentTrack); updateGameplayState(); GameplayClockContainer.FadeInFromZero(750, Easing.OutQuint); StartGameplay(); } /// /// 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. /// protected virtual void StartGameplay() { if (GameplayClockContainer.GameplayClock.IsRunning) throw new InvalidOperationException($"{nameof(StartGameplay)} should not be called when the gameplay clock is already running"); GameplayClockContainer.Reset(); } public override void OnSuspending(IScreen next) { screenSuspension?.Expire(); fadeOut(); base.OnSuspending(next); } public override bool OnExiting(IScreen next) { screenSuspension?.Expire(); // if arriving here and the results screen preparation task hasn't run, it's safe to say the user has not completed the beatmap. if (prepareScoreForDisplayTask == null) { Score.ScoreInfo.Passed = false; // potentially should be ScoreRank.F instead? this is the best alternative for now. Score.ScoreInfo.Rank = ScoreRank.D; } // EndPlaying() is typically called from ReplayRecorder.Dispose(). Disposal is currently asynchronous. // To resolve test failures, forcefully end playing synchronously when this screen exits. // Todo: Replace this with a more permanent solution once osu-framework has a synchronous cleanup method. spectatorClient.EndPlaying(); // 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. (GameplayClockContainer as MasterGameplayClockContainer)?.StopUsingBeatmapClock(); musicController.ResetTrackAdjustments(); fadeOut(); return base.OnExiting(next); } /// /// Creates the player's . /// /// The . protected virtual Score CreateScore() => new Score { ScoreInfo = new ScoreInfo { User = api.LocalUser.Value }, }; /// /// Imports the player's to the local database. /// /// The to import. /// The imported score. protected virtual async Task ImportScore(Score score) { // Replays are already populated and present in the game's database, so should not be re-imported. if (DrawableRuleset.ReplayScore != null) return; LegacyByteArrayReader replayReader; using (var stream = new MemoryStream()) { new LegacyScoreEncoder(score, GameplayBeatmap.PlayableBeatmap).Encode(stream); replayReader = new LegacyByteArrayReader(stream.ToArray(), "replay.osr"); } // 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). long? onlineScoreId = score.ScoreInfo.OnlineScoreID; score.ScoreInfo.OnlineScoreID = null; await scoreManager.Import(score.ScoreInfo, replayReader).ConfigureAwait(false); // ... And restore the online ID for other processes to handle correctly (e.g. de-duplication for the results screen). score.ScoreInfo.OnlineScoreID = onlineScoreId; } /// /// Prepare the for display at results. /// /// The to prepare. /// A task that prepares the provided score. On completion, the score is assumed to be ready for display. protected virtual Task PrepareScoreForResultsAsync(Score score) { // perform one final population to ensure everything is up-to-date. ScoreProcessor.PopulateScore(score.ScoreInfo); return Task.CompletedTask; } /// /// Creates the for a . /// /// The to be displayed in the results screen. /// The . protected virtual ResultsScreen CreateResults(ScoreInfo score) => new SoloResultsScreen(score, true); private void fadeOut(bool instant = false) { float fadeOutDuration = instant ? 0 : 250; this.FadeOut(fadeOutDuration); ApplyToBackground(b => b.IgnoreUserSettings.Value = true); storyboardReplacesBackground.Value = false; } #endregion IBindable ISamplePlaybackDisabler.SamplePlaybackDisabled => samplePlaybackDisabled; } }