diff --git a/osu.Desktop/osu.Desktop.csproj b/osu.Desktop/osu.Desktop.csproj index 874f73da6d..66db439c82 100644 --- a/osu.Desktop/osu.Desktop.csproj +++ b/osu.Desktop/osu.Desktop.csproj @@ -27,8 +27,8 @@ - - + + diff --git a/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj b/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj index feab3ed81c..3f8b3bf086 100644 --- a/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj +++ b/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj @@ -2,9 +2,9 @@ - + - + diff --git a/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj b/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj index e26d2433f9..fd17285a38 100644 --- a/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj +++ b/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj @@ -2,9 +2,9 @@ - + - + diff --git a/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj b/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj index 273d29c3de..8c31db9a7d 100644 --- a/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj +++ b/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj @@ -2,9 +2,9 @@ - + - + diff --git a/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditRuleset.cs b/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditRuleset.cs index 50b3eabcf4..1a6e78d918 100644 --- a/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditRuleset.cs +++ b/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditRuleset.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework.Graphics.Cursor; using osu.Game.Beatmaps; using osu.Game.Rulesets.Osu.UI; using osu.Game.Rulesets.UI; @@ -19,10 +20,7 @@ namespace osu.Game.Rulesets.Osu.Edit private class OsuPlayfieldNoCursor : OsuPlayfield { - public OsuPlayfieldNoCursor() - { - Cursor?.Expire(); - } + protected override CursorContainer CreateCursor() => null; } } } diff --git a/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj b/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj index fade054382..72ce6c947b 100644 --- a/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj +++ b/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj @@ -2,9 +2,9 @@ - + - + diff --git a/osu.Game.Tests/Visual/TestCaseAccountCreationOverlay.cs b/osu.Game.Tests/Visual/TestCaseAccountCreationOverlay.cs index 543a43b439..24380645d1 100644 --- a/osu.Game.Tests/Visual/TestCaseAccountCreationOverlay.cs +++ b/osu.Game.Tests/Visual/TestCaseAccountCreationOverlay.cs @@ -3,9 +3,13 @@ using System; using System.Collections.Generic; +using osu.Framework.Allocation; +using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Game.Online.API; using osu.Game.Overlays; using osu.Game.Overlays.AccountCreation; +using osu.Game.Users; namespace osu.Game.Tests.Visual { @@ -21,12 +25,32 @@ namespace osu.Game.Tests.Visual typeof(AccountCreationScreen), }; + [Cached(typeof(IAPIProvider))] + private DummyAPIAccess api = new DummyAPIAccess(); + public TestCaseAccountCreationOverlay() { - var accountCreation = new AccountCreationOverlay(); - Child = accountCreation; + Container userPanelArea; + AccountCreationOverlay accountCreation; - accountCreation.State = Visibility.Visible; + Children = new Drawable[] + { + api, + accountCreation = new AccountCreationOverlay(), + userPanelArea = new Container + { + Padding = new MarginPadding(10), + AutoSizeAxes = Axes.Both, + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + }, + }; + + api.Logout(); + api.LocalUser.BindValueChanged(user => { userPanelArea.Child = new UserPanel(user.NewValue) { Width = 200 }; }, true); + + AddStep("show", () => accountCreation.State = Visibility.Visible); + AddStep("logout", () => api.Logout()); } } } diff --git a/osu.Game.Tests/Visual/TestCaseBackgroundScreenBeatmap.cs b/osu.Game.Tests/Visual/TestCaseBackgroundScreenBeatmap.cs index a88bf16223..e891e98066 100644 --- a/osu.Game.Tests/Visual/TestCaseBackgroundScreenBeatmap.cs +++ b/osu.Game.Tests/Visual/TestCaseBackgroundScreenBeatmap.cs @@ -183,10 +183,10 @@ namespace osu.Game.Tests.Visual public void PauseTest() { performFullSetup(true); - AddStep("Pause", () => player.CurrentPausableGameplayContainer.Pause()); + AddStep("Pause", () => player.Pause()); waitForDim(); AddAssert("Screen is dimmed and blur applied", () => songSelect.IsBackgroundDimmed() && songSelect.IsUserBlurApplied()); - AddStep("Unpause", () => player.CurrentPausableGameplayContainer.Resume()); + AddStep("Unpause", () => player.Resume()); waitForDim(); AddAssert("Screen is dimmed and blur applied", () => songSelect.IsBackgroundDimmed() && songSelect.IsUserBlurApplied()); } @@ -348,8 +348,6 @@ namespace osu.Game.Tests.Visual }; } - public PausableGameplayContainer CurrentPausableGameplayContainer => PausableGameplayContainer; - public UserDimContainer CurrentStoryboardContainer => StoryboardContainer; // Whether or not the player should be allowed to load. diff --git a/osu.Game.Tests/Visual/TestCaseDisclaimer.cs b/osu.Game.Tests/Visual/TestCaseDisclaimer.cs index f08a2a54ca..8bba16e4b4 100644 --- a/osu.Game.Tests/Visual/TestCaseDisclaimer.cs +++ b/osu.Game.Tests/Visual/TestCaseDisclaimer.cs @@ -16,6 +16,8 @@ namespace osu.Game.Tests.Visual [BackgroundDependencyLoader] private void load() { + Add(api); + AddStep("load disclaimer", () => LoadScreen(new Disclaimer())); AddStep("toggle support", () => diff --git a/osu.Game.Tests/Visual/TestCaseGameplayMenuOverlay.cs b/osu.Game.Tests/Visual/TestCaseGameplayMenuOverlay.cs index 93a059d214..c5ad57fec9 100644 --- a/osu.Game.Tests/Visual/TestCaseGameplayMenuOverlay.cs +++ b/osu.Game.Tests/Visual/TestCaseGameplayMenuOverlay.cs @@ -17,15 +17,15 @@ namespace osu.Game.Tests.Visual [Description("player pause/fail screens")] public class TestCaseGameplayMenuOverlay : ManualInputManagerTestCase { - public override IReadOnlyList RequiredTypes => new[] { typeof(FailOverlay), typeof(PausableGameplayContainer) }; + public override IReadOnlyList RequiredTypes => new[] { typeof(FailOverlay), typeof(PauseOverlay) }; private FailOverlay failOverlay; - private PausableGameplayContainer.PauseOverlay pauseOverlay; + private PauseOverlay pauseOverlay; [BackgroundDependencyLoader] private void load() { - Add(pauseOverlay = new PausableGameplayContainer.PauseOverlay + Add(pauseOverlay = new PauseOverlay { OnResume = () => Logger.Log(@"Resume"), OnRetry = () => Logger.Log(@"Retry"), diff --git a/osu.Game.Tests/Visual/TestCasePause.cs b/osu.Game.Tests/Visual/TestCasePause.cs new file mode 100644 index 0000000000..d5d2cebbab --- /dev/null +++ b/osu.Game.Tests/Visual/TestCasePause.cs @@ -0,0 +1,152 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Framework.Graphics.Containers; +using osu.Framework.Screens; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Scoring; +using osu.Game.Screens.Play; + +namespace osu.Game.Tests.Visual +{ + public class TestCasePause : PlayerTestCase + { + protected new PausePlayer Player => (PausePlayer)base.Player; + + public TestCasePause() + : base(new OsuRuleset()) + { + } + + [Test] + public void TestPauseResume() + { + pauseAndConfirm(); + resumeAndConfirm(); + } + + [Test] + public void TestPauseTooSoon() + { + pauseAndConfirm(); + resumeAndConfirm(); + + pause(); + + confirmClockRunning(true); + confirmPauseOverlayShown(false); + } + + [Test] + public void TestExitTooSoon() + { + pauseAndConfirm(); + + resume(); + + AddStep("exit too soon", () => Player.Exit()); + + confirmClockRunning(true); + confirmPauseOverlayShown(false); + + AddAssert("not exited", () => Player.IsCurrentScreen()); + } + + [Test] + public void TestPauseAfterFail() + { + AddUntilStep("wait for fail", () => Player.HasFailed); + AddAssert("fail overlay shown", () => Player.FailOverlayVisible); + + confirmClockRunning(false); + + pause(); + + confirmClockRunning(false); + confirmPauseOverlayShown(false); + + AddAssert("fail overlay still shown", () => Player.FailOverlayVisible); + + exitAndConfirm(); + } + + [Test] + public void TestExitFromGameplay() + { + AddStep("exit", () => Player.Exit()); + + confirmPaused(); + + exitAndConfirm(); + } + + [Test] + public void TestExitFromPause() + { + pauseAndConfirm(); + exitAndConfirm(); + } + + private void pauseAndConfirm() + { + pause(); + confirmPaused(); + } + + private void resumeAndConfirm() + { + resume(); + confirmResumed(); + } + + private void exitAndConfirm() + { + AddUntilStep("player not exited", () => Player.IsCurrentScreen()); + AddStep("exit", () => Player.Exit()); + confirmExited(); + } + + private void confirmPaused() + { + confirmClockRunning(false); + AddAssert("pause overlay shown", () => Player.PauseOverlayVisible); + } + + private void confirmResumed() + { + confirmClockRunning(true); + confirmPauseOverlayShown(false); + } + + private void confirmExited() + { + AddUntilStep("player exited", () => !Player.IsCurrentScreen()); + } + + private void pause() => AddStep("pause", () => Player.Pause()); + private void resume() => AddStep("resume", () => Player.Resume()); + + private void confirmPauseOverlayShown(bool isShown) => + AddAssert("pause overlay " + (isShown ? "shown" : "hidden"), () => Player.PauseOverlayVisible == isShown); + + private void confirmClockRunning(bool isRunning) => + AddAssert("clock " + (isRunning ? "running" : "stopped"), () => Player.GameplayClockContainer.GameplayClock.IsRunning == isRunning); + + protected override bool AllowFail => true; + + protected override Player CreatePlayer(Ruleset ruleset) => new PausePlayer(); + + protected class PausePlayer : Player + { + public new GameplayClockContainer GameplayClockContainer => base.GameplayClockContainer; + + public new ScoreProcessor ScoreProcessor => base.ScoreProcessor; + + public bool FailOverlayVisible => FailOverlay.State == Visibility.Visible; + + public bool PauseOverlayVisible => PauseOverlay.State == Visibility.Visible; + } + } +} diff --git a/osu.Game.Tests/osu.Game.Tests.csproj b/osu.Game.Tests/osu.Game.Tests.csproj index b22c1aed99..938e1ae0f8 100644 --- a/osu.Game.Tests/osu.Game.Tests.csproj +++ b/osu.Game.Tests/osu.Game.Tests.csproj @@ -3,9 +3,9 @@ - + - + diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs index 3d861e44bf..c5f6ef41c2 100644 --- a/osu.Game/Online/API/APIAccess.cs +++ b/osu.Game/Online/API/APIAccess.cs @@ -266,20 +266,18 @@ namespace osu.Game.Online.API get => state; private set { - APIState oldState = state; - APIState newState = value; + if (state == value) + return; + APIState oldState = state; state = value; - if (oldState != newState) + log.Add($@"We just went {state}!"); + Scheduler.Add(delegate { - log.Add($@"We just went {newState}!"); - Scheduler.Add(delegate - { - components.ForEach(c => c.APIStateChanged(this, newState)); - OnStateChange?.Invoke(oldState, newState); - }); - } + components.ForEach(c => c.APIStateChanged(this, state)); + OnStateChange?.Invoke(oldState, state); + }); } } diff --git a/osu.Game/Online/API/DummyAPIAccess.cs b/osu.Game/Online/API/DummyAPIAccess.cs index 0cb49951f7..99fde10309 100644 --- a/osu.Game/Online/API/DummyAPIAccess.cs +++ b/osu.Game/Online/API/DummyAPIAccess.cs @@ -1,12 +1,15 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Collections.Generic; +using System.Threading; using osu.Framework.Bindables; +using osu.Framework.Graphics; using osu.Game.Users; namespace osu.Game.Online.API { - public class DummyAPIAccess : IAPIProvider + public class DummyAPIAccess : Component, IAPIProvider { public Bindable LocalUser { get; } = new Bindable(new User { @@ -20,7 +23,23 @@ namespace osu.Game.Online.API public string Endpoint => "http://localhost"; - public APIState State => LocalUser.Value.Id == 1 ? APIState.Offline : APIState.Online; + private APIState state = APIState.Online; + + private readonly List components = new List(); + + public APIState State + { + get => state; + private set + { + if (state == value) + return; + + state = value; + + Scheduler.Add(() => components.ForEach(c => c.APIStateChanged(this, value))); + } + } public virtual void Queue(APIRequest request) { @@ -28,28 +47,36 @@ namespace osu.Game.Online.API public void Register(IOnlineComponent component) { - // todo: add support + Scheduler.Add(delegate { components.Add(component); }); + component.APIStateChanged(this, state); } public void Unregister(IOnlineComponent component) { - // todo: add support + Scheduler.Add(delegate { components.Remove(component); }); } public void Login(string username, string password) { LocalUser.Value = new User { - Username = @"Dummy", + Username = username, Id = 1001, }; + + State = APIState.Online; } public void Logout() { LocalUser.Value = new GuestUser(); + State = APIState.Offline; } - public RegistrationRequest.RegistrationRequestErrors CreateAccount(string email, string username, string password) => null; + public RegistrationRequest.RegistrationRequestErrors CreateAccount(string email, string username, string password) + { + Thread.Sleep(200); + return null; + } } } diff --git a/osu.Game/Rulesets/UI/DrawableRuleset.cs b/osu.Game/Rulesets/UI/DrawableRuleset.cs index abf0fb2696..31c0afd743 100644 --- a/osu.Game/Rulesets/UI/DrawableRuleset.cs +++ b/osu.Game/Rulesets/UI/DrawableRuleset.cs @@ -169,6 +169,8 @@ namespace osu.Game.Rulesets.UI mod.ApplyToDrawableHitObjects(Playfield.HitObjectContainer.Objects); } + public override void RequestResume(Action continueResume) => continueResume(); + /// /// Creates and adds the visual representation of a to this . /// @@ -339,6 +341,13 @@ namespace osu.Game.Rulesets.UI /// The replay, null for local input. public abstract void SetReplayScore(Score replayScore); + /// + /// Invoked when the interactive user requests resuming from a paused state. + /// Allows potentially delaying the resume process until an interaction is performed. + /// + /// The action to run when resuming is to be completed. + public abstract void RequestResume(Action continueResume); + /// /// Create a for the associated ruleset and link with this /// . diff --git a/osu.Game/Screens/Play/GameplayClockContainer.cs b/osu.Game/Screens/Play/GameplayClockContainer.cs index 3c2cec1d94..deac5e02bf 100644 --- a/osu.Game/Screens/Play/GameplayClockContainer.cs +++ b/osu.Game/Screens/Play/GameplayClockContainer.cs @@ -18,7 +18,7 @@ using osu.Game.Rulesets.Mods; namespace osu.Game.Screens.Play { /// - /// Encapsulates gameplay timing logic and provides a for children. + /// Encapsulates gameplay timing logic and provides a for children. /// public class GameplayClockContainer : Container { @@ -48,7 +48,7 @@ namespace osu.Game.Screens.Play /// The final clock which is exposed to underlying components. /// [Cached] - private readonly GameplayClock gameplayClock; + public readonly GameplayClock GameplayClock; private Bindable userAudioOffset; @@ -78,7 +78,7 @@ namespace osu.Game.Screens.Play offsetClock = new FramedOffsetClock(platformOffsetClock); // the clock to be exposed via DI to children. - gameplayClock = new GameplayClock(offsetClock); + GameplayClock = new GameplayClock(offsetClock); } [BackgroundDependencyLoader] @@ -118,11 +118,16 @@ namespace osu.Game.Screens.Play // This accounts for the audio clock source potentially taking time to enter a completely stopped state adjustableClock.Seek(adjustableClock.CurrentTime); adjustableClock.Start(); + IsPaused.Value = false; } public void Seek(double time) => adjustableClock.Seek(time); - public void Stop() => adjustableClock.Stop(); + public void Stop() + { + adjustableClock.Stop(); + IsPaused.Value = true; + } public void ResetLocalAdjustments() { diff --git a/osu.Game/Screens/Play/PausableGameplayContainer.cs b/osu.Game/Screens/Play/PausableGameplayContainer.cs deleted file mode 100644 index 99f0083b55..0000000000 --- a/osu.Game/Screens/Play/PausableGameplayContainer.cs +++ /dev/null @@ -1,137 +0,0 @@ -// 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.Linq; -using osu.Framework.Allocation; -using osu.Framework.Bindables; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Game.Graphics; -using osuTK.Graphics; - -namespace osu.Game.Screens.Play -{ - /// - /// A container which handles pausing children, displaying an overlay blocking its children during paused state. - /// - public class PausableGameplayContainer : Container - { - public readonly BindableBool IsPaused = new BindableBool(); - - public Func CheckCanPause; - - private const double pause_cooldown = 1000; - private double lastPauseActionTime; - - private readonly PauseOverlay pauseOverlay; - - private readonly Container content; - - protected override Container Content => content; - - public int Retries - { - set => pauseOverlay.Retries = value; - } - - public bool CanPause => (CheckCanPause?.Invoke() ?? true) && Time.Current >= lastPauseActionTime + pause_cooldown; - public bool IsResuming { get; private set; } - - public Action OnRetry; - public Action OnQuit; - - public Action Stop; - public Action Start; - - /// - /// Creates a new . - /// - public PausableGameplayContainer() - { - RelativeSizeAxes = Axes.Both; - - InternalChildren = new[] - { - content = new Container - { - RelativeSizeAxes = Axes.Both - }, - pauseOverlay = new PauseOverlay - { - OnResume = () => - { - IsResuming = true; - this.Delay(400).Schedule(Resume); - }, - OnRetry = () => OnRetry(), - OnQuit = () => OnQuit(), - } - }; - } - - public void Pause(bool force = false) => Schedule(() => // Scheduled to ensure a stable position in execution order, no matter how it was called. - { - if (!CanPause && !force) return; - - if (IsPaused.Value) return; - - // stop the seekable clock (stops the audio eventually) - Stop?.Invoke(); - IsPaused.Value = true; - - pauseOverlay.Show(); - - lastPauseActionTime = Time.Current; - }); - - public void Resume() - { - if (!IsPaused.Value) return; - - IsResuming = false; - lastPauseActionTime = Time.Current; - - IsPaused.Value = false; - - Start?.Invoke(); - - pauseOverlay.Hide(); - } - - private OsuGameBase game; - - [BackgroundDependencyLoader] - private void load(OsuGameBase game) - { - this.game = game; - } - - protected override void Update() - { - // eagerly pause when we lose window focus (if we are locally playing). - if (!game.IsActive.Value && CanPause) - Pause(); - - base.Update(); - } - - public class PauseOverlay : GameplayMenuOverlay - { - public Action OnResume; - - public override string Header => "paused"; - public override string Description => "you're not going to do what i think you're going to do, are ya?"; - - protected override Action BackAction => () => InternalButtons.Children.First().Click(); - - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - AddButton("Continue", colours.Green, () => OnResume?.Invoke()); - AddButton("Retry", colours.YellowDark, () => OnRetry?.Invoke()); - AddButton("Quit", new Color4(170, 27, 39, 255), () => OnQuit?.Invoke()); - } - } - } -} diff --git a/osu.Game/Screens/Play/PauseOverlay.cs b/osu.Game/Screens/Play/PauseOverlay.cs new file mode 100644 index 0000000000..6cc6027a03 --- /dev/null +++ b/osu.Game/Screens/Play/PauseOverlay.cs @@ -0,0 +1,29 @@ +// 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.Linq; +using osu.Framework.Allocation; +using osu.Game.Graphics; +using osuTK.Graphics; + +namespace osu.Game.Screens.Play +{ + public class PauseOverlay : GameplayMenuOverlay + { + public Action OnResume; + + public override string Header => "paused"; + public override string Description => "you're not going to do what i think you're going to do, are ya?"; + + protected override Action BackAction => () => InternalButtons.Children.First().Click(); + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + AddButton("Continue", colours.Green, () => OnResume?.Invoke()); + AddButton("Retry", colours.YellowDark, () => OnRetry?.Invoke()); + AddButton("Quit", new Color4(170, 27, 39, 255), () => OnQuit?.Invoke()); + } + } +} diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 194d09f4b3..7b1cdd21a6 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -56,8 +56,6 @@ namespace osu.Game.Screens.Play [Resolved] private ScoreManager scoreManager { get; set; } - protected PausableGameplayContainer PausableGameplayContainer { get; private set; } - private RulesetInfo ruleset; private IAPIProvider api; @@ -68,23 +66,10 @@ namespace osu.Game.Screens.Play protected DrawableRuleset DrawableRuleset { get; private set; } protected HUDOverlay HUDOverlay { get; private set; } - private FailOverlay failOverlay; - - private DrawableStoryboard storyboard; - protected UserDimContainer StoryboardContainer { get; private set; } - - private Bindable showStoryboard; - - protected virtual UserDimContainer CreateStoryboardContainer() => new UserDimContainer(true) - { - RelativeSizeAxes = Axes.Both, - Alpha = 1, - EnableUserDim = { Value = true } - }; public bool LoadedBeatmapSuccessfully => DrawableRuleset?.Objects.Any() == true; - private GameplayClockContainer gameplayClockContainer; + protected GameplayClockContainer GameplayClockContainer { get; private set; } [BackgroundDependencyLoader] private void load(AudioManager audio, IAPIProvider api, OsuConfigManager config) @@ -105,55 +90,49 @@ namespace osu.Game.Screens.Play if (!ScoreProcessor.Mode.Disabled) config.BindWith(OsuSetting.ScoreDisplayMode, ScoreProcessor.Mode); - InternalChild = gameplayClockContainer = new GameplayClockContainer(working, AllowLeadIn, DrawableRuleset.GameplayStartTime); + InternalChild = GameplayClockContainer = new GameplayClockContainer(working, AllowLeadIn, DrawableRuleset.GameplayStartTime); - gameplayClockContainer.Children = new Drawable[] + GameplayClockContainer.Children = new[] { - PausableGameplayContainer = new PausableGameplayContainer + StoryboardContainer = CreateStoryboardContainer(), + new ScalingContainer(ScalingMode.Gameplay) { - Retries = RestartCount, - OnRetry = Restart, - OnQuit = performUserRequestedExit, - Start = gameplayClockContainer.Start, - Stop = gameplayClockContainer.Stop, - IsPaused = { BindTarget = gameplayClockContainer.IsPaused }, - CheckCanPause = () => AllowPause && ValidForResume && !HasFailed && !DrawableRuleset.HasReplayLoaded.Value, - Children = new[] + Child = new LocalSkinOverrideContainer(working.Skin) { - StoryboardContainer = CreateStoryboardContainer(), - new ScalingContainer(ScalingMode.Gameplay) - { - Child = new LocalSkinOverrideContainer(working.Skin) - { - RelativeSizeAxes = Axes.Both, - Child = DrawableRuleset - } - }, - new BreakOverlay(working.Beatmap.BeatmapInfo.LetterboxInBreaks, ScoreProcessor) - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Breaks = working.Beatmap.Breaks - }, - // display the cursor above some HUD elements. - DrawableRuleset.Cursor?.CreateProxy() ?? new Container(), - HUDOverlay = new HUDOverlay(ScoreProcessor, DrawableRuleset, working) - { - HoldToQuit = { Action = performUserRequestedExit }, - PlayerSettingsOverlay = { PlaybackSettings = { UserPlaybackRate = { BindTarget = gameplayClockContainer.UserPlaybackRate } } }, - KeyCounter = { Visible = { BindTarget = DrawableRuleset.HasReplayLoaded } }, - RequestSeek = gameplayClockContainer.Seek, - Anchor = Anchor.Centre, - Origin = Anchor.Centre - }, - new SkipOverlay(DrawableRuleset.GameplayStartTime) - { - RequestSeek = gameplayClockContainer.Seek - }, + RelativeSizeAxes = Axes.Both, + Child = DrawableRuleset } }, - failOverlay = new FailOverlay + new BreakOverlay(working.Beatmap.BeatmapInfo.LetterboxInBreaks, ScoreProcessor) { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Breaks = working.Beatmap.Breaks + }, + // display the cursor above some HUD elements. + DrawableRuleset.Cursor?.CreateProxy() ?? new Container(), + HUDOverlay = new HUDOverlay(ScoreProcessor, DrawableRuleset, working) + { + HoldToQuit = { Action = performUserRequestedExit }, + PlayerSettingsOverlay = { PlaybackSettings = { UserPlaybackRate = { BindTarget = GameplayClockContainer.UserPlaybackRate } } }, + KeyCounter = { Visible = { BindTarget = DrawableRuleset.HasReplayLoaded } }, + RequestSeek = GameplayClockContainer.Seek, + Anchor = Anchor.Centre, + Origin = Anchor.Centre + }, + new SkipOverlay(DrawableRuleset.GameplayStartTime) + { + RequestSeek = GameplayClockContainer.Seek + }, + FailOverlay = new FailOverlay + { + OnRetry = Restart, + OnQuit = performUserRequestedExit, + }, + PauseOverlay = new PauseOverlay + { + OnResume = Resume, + Retries = RestartCount, OnRetry = Restart, OnQuit = performUserRequestedExit, }, @@ -170,10 +149,10 @@ namespace osu.Game.Screens.Play }; // bind clock into components that require it - DrawableRuleset.IsPaused.BindTo(gameplayClockContainer.IsPaused); + DrawableRuleset.IsPaused.BindTo(GameplayClockContainer.IsPaused); - if (showStoryboard.Value) - initializeStoryboard(false); + // load storyboard as part of player's load if we can + initializeStoryboard(false); // Bind ScoreProcessor to ourselves ScoreProcessor.AllJudged += onCompletion; @@ -289,19 +268,148 @@ namespace osu.Game.Screens.Play return score; } + protected override bool OnScroll(ScrollEvent e) => mouseWheelDisabled.Value && !GameplayClockContainer.IsPaused.Value; + + protected virtual Results CreateResults(ScoreInfo score) => new SoloResults(score); + + #region Storyboard + + private DrawableStoryboard storyboard; + protected UserDimContainer StoryboardContainer { get; private set; } + + protected virtual UserDimContainer CreateStoryboardContainer() => new UserDimContainer(true) + { + RelativeSizeAxes = Axes.Both, + Alpha = 1, + EnableUserDim = { Value = true } + }; + + private Bindable showStoryboard; + + private void initializeStoryboard(bool asyncLoad) + { + if (StoryboardContainer == null || storyboard != null) + return; + + if (!showStoryboard.Value) + return; + + var beatmap = Beatmap.Value; + + storyboard = beatmap.Storyboard.CreateDrawable(); + storyboard.Masking = true; + + if (asyncLoad) + LoadComponentAsync(storyboard, StoryboardContainer.Add); + else + StoryboardContainer.Add(storyboard); + } + + #endregion + + #region Fail Logic + + protected FailOverlay FailOverlay { get; private set; } + private bool onFail() { if (Beatmap.Value.Mods.Value.OfType().Any(m => !m.AllowFail)) return false; - gameplayClockContainer.Stop(); + GameplayClockContainer.Stop(); HasFailed = true; - failOverlay.Retries = RestartCount; - failOverlay.Show(); + + // 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 == Visibility.Visible) + PauseOverlay.Hide(); + + FailOverlay.Retries = RestartCount; + FailOverlay.Show(); return true; } + #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; + + private bool canPause => + // must pass basic screen conditions (beatmap loaded, instance allows pause) + LoadedBeatmapSuccessfully && AllowPause && ValidForResume + // replays cannot be paused and exit immediately + && !DrawableRuleset.HasReplayLoaded.Value + // cannot pause if we are already in a fail state + && !HasFailed + // cannot pause if already paused (or in a cooldown state) unless we are in a resuming state. + && (IsResuming || (GameplayClockContainer.IsPaused.Value == false && !pauseCooldownActive)); + + private bool pauseCooldownActive => + lastPauseActionTime.HasValue && GameplayClockContainer.GameplayClock.CurrentTime < lastPauseActionTime + pause_cooldown; + + 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; + + protected override void Update() + { + base.Update(); + + // eagerly pause when we lose window focus (if we are locally playing). + if (!Game.IsActive.Value) + Pause(); + } + + public void Pause() + { + if (!canPause) return; + + IsResuming = false; + GameplayClockContainer.Stop(); + PauseOverlay.Show(); + lastPauseActionTime = GameplayClockContainer.GameplayClock.CurrentTime; + } + + public void Resume() + { + if (!canResume) return; + + IsResuming = true; + PauseOverlay.Hide(); + + // time-based conditions may allow instant resume. + if (GameplayClockContainer.GameplayClock.CurrentTime < Beatmap.Value.Beatmap.HitObjects.First().StartTime) + completeResume(); + else + DrawableRuleset.RequestResume(completeResume); + + void completeResume() + { + GameplayClockContainer.Start(); + IsResuming = false; + } + } + + #endregion + + #region Screen Logic + public override void OnEntering(IScreen last) { base.OnEntering(last); @@ -316,10 +424,7 @@ namespace osu.Game.Screens.Play .Delay(250) .FadeIn(250); - showStoryboard.ValueChanged += enabled => - { - if (enabled.NewValue) initializeStoryboard(true); - }; + showStoryboard.ValueChanged += _ => initializeStoryboard(true); Background.EnableUserDim.Value = true; Background.BlurAmount.Value = 0; @@ -329,10 +434,8 @@ namespace osu.Game.Screens.Play storyboardReplacesBackground.Value = Beatmap.Value.Storyboard.ReplacesBackground && Beatmap.Value.Storyboard.HasDrawable; - gameplayClockContainer.Restart(); - - PausableGameplayContainer.Alpha = 0; - PausableGameplayContainer.FadeIn(750, Easing.OutQuint); + GameplayClockContainer.Restart(); + GameplayClockContainer.FadeInFromZero(750, Easing.OutQuint); } public override void OnSuspending(IScreen next) @@ -350,18 +453,20 @@ namespace osu.Game.Screens.Play return true; } - if ((!AllowPause || HasFailed || !ValidForResume || PausableGameplayContainer?.IsPaused.Value != false || DrawableRuleset?.HasReplayLoaded.Value != false) && (!PausableGameplayContainer?.IsResuming ?? true)) + if (canPause) { - gameplayClockContainer.ResetLocalAdjustments(); - - fadeOut(); - return base.OnExiting(next); + Pause(); + return true; } - if (LoadedBeatmapSuccessfully) - PausableGameplayContainer?.Pause(); + if (pauseCooldownActive && !GameplayClockContainer.IsPaused.Value) + // still want to block if we are within the cooldown period and not already paused. + return true; - return true; + GameplayClockContainer.ResetLocalAdjustments(); + + fadeOut(); + return base.OnExiting(next); } private void fadeOut(bool instant = false) @@ -373,24 +478,6 @@ namespace osu.Game.Screens.Play storyboardReplacesBackground.Value = false; } - protected override bool OnScroll(ScrollEvent e) => mouseWheelDisabled.Value && !PausableGameplayContainer.IsPaused.Value; - - private void initializeStoryboard(bool asyncLoad) - { - if (StoryboardContainer == null || storyboard != null) - return; - - var beatmap = Beatmap.Value; - - storyboard = beatmap.Storyboard.CreateDrawable(); - storyboard.Masking = true; - - if (asyncLoad) - LoadComponentAsync(storyboard, StoryboardContainer.Add); - else - StoryboardContainer.Add(storyboard); - } - - protected virtual Results CreateResults(ScoreInfo score) => new SoloResults(score); + #endregion } } diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index bfd1d3d236..d7240a40ad 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -55,9 +55,9 @@ namespace osu.Game.Screens.Select public override bool HandlePositionalInput => AllowSelection; /// - /// Used to avoid firing null selections before the initial beatmaps have been loaded via . + /// Whether carousel items have completed asynchronously loaded. /// - private bool initialLoadComplete; + public bool BeatmapSetsLoaded { get; private set; } private IEnumerable beatmapSets => root.Children.OfType(); @@ -90,7 +90,7 @@ namespace osu.Game.Screens.Select Schedule(() => { BeatmapSetsChanged?.Invoke(); - initialLoadComplete = true; + BeatmapSetsLoaded = true; }); })); } @@ -327,6 +327,9 @@ namespace osu.Game.Screens.Select private void select(CarouselItem item) { + if (!AllowSelection) + return; + if (item == null) return; item.State.Value = CarouselItemState.Selected; @@ -593,7 +596,7 @@ namespace osu.Game.Screens.Select currentY += DrawHeight / 2; scrollableContent.Height = currentY; - if (initialLoadComplete && (selectedBeatmapSet == null || selectedBeatmap == null || selectedBeatmapSet.State.Value != CarouselItemState.Selected)) + if (BeatmapSetsLoaded && (selectedBeatmapSet == null || selectedBeatmap == null || selectedBeatmapSet.State.Value != CarouselItemState.Selected)) { selectedBeatmapSet = null; SelectionChanged?.Invoke(null); diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index a86d0beb39..8758df5151 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -300,6 +300,10 @@ namespace osu.Game.Screens.Select /// Whether to trigger . public void FinaliseSelection(BeatmapInfo beatmap = null, bool performStartAction = true) { + // This is very important as we have not yet bound to screen-level bindables before the carousel load is completed. + if (!Carousel.BeatmapSetsLoaded) + return; + // if we have a pending filter operation, we want to run it now. // it could change selection (ie. if the ruleset has been changed). Carousel.FlushPendingFilterOperations(); @@ -373,6 +377,13 @@ namespace osu.Game.Screens.Select var beatmap = beatmapNoDebounce; var ruleset = rulesetNoDebounce; + selectionChangedDebounce?.Cancel(); + + if (beatmap == null) + run(); + else + selectionChangedDebounce = Scheduler.AddDelayed(run, 200); + void run() { Logger.Log($"updating selection with beatmap:{beatmap?.ID.ToString() ?? "null"} ruleset:{ruleset?.ID.ToString() ?? "null"}"); @@ -417,13 +428,6 @@ namespace osu.Game.Screens.Select if (this.IsCurrentScreen()) ensurePlayingSelected(preview); UpdateBeatmap(Beatmap.Value); } - - selectionChangedDebounce?.Cancel(); - - if (beatmap == null) - run(); - else - selectionChangedDebounce = Scheduler.AddDelayed(run, 200); } private void triggerRandom() @@ -593,18 +597,7 @@ namespace osu.Game.Screens.Select private void carouselBeatmapsLoaded() { - if (rulesetNoDebounce == null) - { - // manual binding to parent ruleset to allow for delayed load in the incoming direction. - rulesetNoDebounce = decoupledRuleset.Value = Ruleset.Value; - Ruleset.ValueChanged += r => updateSelectedRuleset(r.NewValue); - - decoupledRuleset.ValueChanged += r => Ruleset.Value = r.NewValue; - decoupledRuleset.DisabledChanged += r => Ruleset.Disabled = r; - - Beatmap.BindDisabledChanged(disabled => Carousel.AllowSelection = !disabled, true); - Beatmap.BindValueChanged(workingBeatmapChanged); - } + bindBindables(); if (!Beatmap.IsDefault && Beatmap.Value.BeatmapSetInfo?.DeletePending == false && Beatmap.Value.BeatmapSetInfo?.Protected == false && Carousel.SelectBeatmap(Beatmap.Value.BeatmapInfo, false)) @@ -618,6 +611,26 @@ namespace osu.Game.Screens.Select } } + private bool boundLocalBindables; + + private void bindBindables() + { + if (boundLocalBindables) + return; + + // manual binding to parent ruleset to allow for delayed load in the incoming direction. + rulesetNoDebounce = decoupledRuleset.Value = Ruleset.Value; + Ruleset.ValueChanged += r => updateSelectedRuleset(r.NewValue); + + decoupledRuleset.ValueChanged += r => Ruleset.Value = r.NewValue; + decoupledRuleset.DisabledChanged += r => Ruleset.Disabled = r; + + Beatmap.BindDisabledChanged(disabled => Carousel.AllowSelection = !disabled, true); + Beatmap.BindValueChanged(workingBeatmapChanged); + + boundLocalBindables = true; + } + private void delete(BeatmapSetInfo beatmap) { if (beatmap == null || beatmap.ID <= 0) return; diff --git a/osu.Game/Tests/Visual/TestCasePlayer.cs b/osu.Game/Tests/Visual/TestCasePlayer.cs deleted file mode 100644 index db46421a0b..0000000000 --- a/osu.Game/Tests/Visual/TestCasePlayer.cs +++ /dev/null @@ -1,127 +0,0 @@ -// 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.Linq; -using osu.Framework.Allocation; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Lists; -using osu.Framework.Screens; -using osu.Game.Beatmaps; -using osu.Game.Rulesets; -using osu.Game.Rulesets.Mods; -using osu.Game.Screens.Play; -using osu.Game.Tests.Beatmaps; -using osuTK.Graphics; - -namespace osu.Game.Tests.Visual -{ - public abstract class TestCasePlayer : RateAdjustedBeatmapTestCase - { - private readonly Ruleset ruleset; - - protected Player Player; - - protected TestCasePlayer(Ruleset ruleset) - { - this.ruleset = ruleset; - } - - protected TestCasePlayer() - { - } - - [BackgroundDependencyLoader] - private void load(RulesetStore rulesets) - { - Add(new Box - { - RelativeSizeAxes = Framework.Graphics.Axes.Both, - Colour = Color4.Black, - Depth = int.MaxValue - }); - - if (ruleset != null) - { - Player p = null; - AddStep(ruleset.RulesetInfo.Name, () => p = loadPlayerFor(ruleset)); - AddCheckSteps(() => p); - } - else - { - foreach (var r in rulesets.AvailableRulesets) - { - Player p = null; - AddStep(r.Name, () => p = loadPlayerFor(r)); - AddCheckSteps(() => p); - - AddUntilStep("no leaked beatmaps", () => - { - p = null; - - GC.Collect(); - GC.WaitForPendingFinalizers(); - int count = 0; - - workingWeakReferences.ForEachAlive(_ => count++); - return count == 1; - }); - - AddUntilStep("no leaked players", () => - { - GC.Collect(); - GC.WaitForPendingFinalizers(); - int count = 0; - - playerWeakReferences.ForEachAlive(_ => count++); - return count == 1; - }); - } - } - } - - protected virtual void AddCheckSteps(Func player) - { - AddUntilStep("player loaded", () => player().IsLoaded); - } - - protected virtual IBeatmap CreateBeatmap(Ruleset ruleset) => new TestBeatmap(ruleset.RulesetInfo); - - private readonly WeakList workingWeakReferences = new WeakList(); - private readonly WeakList playerWeakReferences = new WeakList(); - - private Player loadPlayerFor(RulesetInfo ri) - { - Ruleset.Value = ri; - return loadPlayerFor(ri.CreateInstance()); - } - - private Player loadPlayerFor(Ruleset r) - { - var beatmap = CreateBeatmap(r); - var working = new TestWorkingBeatmap(beatmap, Clock); - - workingWeakReferences.Add(working); - - Beatmap.Value = working; - Beatmap.Value.Mods.Value = new[] { r.GetAllMods().First(m => m is ModNoFail) }; - - Player?.Exit(); - - Player = CreatePlayer(r); - - playerWeakReferences.Add(Player); - - LoadScreen(Player); - - return Player; - } - - protected virtual Player CreatePlayer(Ruleset ruleset) => new Player - { - AllowPause = false, - AllowLeadIn = false, - AllowResults = false, - }; - } -} diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index d8561770fd..dd69faad56 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -11,13 +11,13 @@ - - - + + + - +