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 @@
-
-
-
+
+
+
-
+