mirror of
https://github.com/ppy/osu.git
synced 2025-01-26 12:45:09 +08:00
Merge pull request #11713 from peppy/fix-error-exit-during-results-transition
Fix a potential crash when exiting play during the results screen transition
This commit is contained in:
commit
52372fe50d
@ -56,9 +56,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
pauseAndConfirm();
|
pauseAndConfirm();
|
||||||
resume();
|
resume();
|
||||||
|
|
||||||
confirmClockRunning(false);
|
confirmPausedWithNoOverlay();
|
||||||
confirmPauseOverlayShown(false);
|
|
||||||
|
|
||||||
AddStep("click to resume", () => InputManager.Click(MouseButton.Left));
|
AddStep("click to resume", () => InputManager.Click(MouseButton.Left));
|
||||||
|
|
||||||
confirmClockRunning(true);
|
confirmClockRunning(true);
|
||||||
@ -71,15 +69,14 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
AddUntilStep("wait for hitobjects", () => Player.HealthProcessor.Health.Value < 1);
|
AddUntilStep("wait for hitobjects", () => Player.HealthProcessor.Health.Value < 1);
|
||||||
|
|
||||||
pauseAndConfirm();
|
pauseAndConfirm();
|
||||||
|
|
||||||
resume();
|
resume();
|
||||||
confirmClockRunning(false);
|
|
||||||
confirmPauseOverlayShown(false);
|
|
||||||
|
|
||||||
|
confirmPausedWithNoOverlay();
|
||||||
pauseAndConfirm();
|
pauseAndConfirm();
|
||||||
|
|
||||||
AddUntilStep("resume overlay is not active", () => Player.DrawableRuleset.ResumeOverlay.State.Value == Visibility.Hidden);
|
AddUntilStep("resume overlay is not active", () => Player.DrawableRuleset.ResumeOverlay.State.Value == Visibility.Hidden);
|
||||||
confirmPaused();
|
confirmPaused();
|
||||||
|
confirmNotExited();
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -94,33 +91,45 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestPauseTooSoon()
|
public void TestUserPauseDuringCooldownTooSoon()
|
||||||
{
|
{
|
||||||
AddStep("move cursor outside", () => InputManager.MoveMouseTo(Player.ScreenSpaceDrawQuad.TopLeft - new Vector2(10)));
|
AddStep("move cursor outside", () => InputManager.MoveMouseTo(Player.ScreenSpaceDrawQuad.TopLeft - new Vector2(10)));
|
||||||
|
|
||||||
pauseAndConfirm();
|
pauseAndConfirm();
|
||||||
|
|
||||||
resume();
|
resume();
|
||||||
pause();
|
pauseFromUserExitKey();
|
||||||
|
|
||||||
confirmClockRunning(true);
|
confirmResumed();
|
||||||
confirmPauseOverlayShown(false);
|
confirmNotExited();
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestExitTooSoon()
|
public void TestQuickExitDuringCooldownTooSoon()
|
||||||
|
{
|
||||||
|
AddStep("move cursor outside", () => InputManager.MoveMouseTo(Player.ScreenSpaceDrawQuad.TopLeft - new Vector2(10)));
|
||||||
|
|
||||||
|
pauseAndConfirm();
|
||||||
|
|
||||||
|
resume();
|
||||||
|
AddStep("pause via exit key", () => Player.ExitViaQuickExit());
|
||||||
|
|
||||||
|
confirmResumed();
|
||||||
|
AddAssert("exited", () => !Player.IsCurrentScreen());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestExitSoonAfterResumeSucceeds()
|
||||||
{
|
{
|
||||||
AddStep("seek before gameplay", () => Player.GameplayClockContainer.Seek(-5000));
|
AddStep("seek before gameplay", () => Player.GameplayClockContainer.Seek(-5000));
|
||||||
|
|
||||||
pauseAndConfirm();
|
pauseAndConfirm();
|
||||||
resume();
|
resume();
|
||||||
|
|
||||||
AddStep("exit too soon", () => Player.Exit());
|
AddStep("exit quick", () => Player.Exit());
|
||||||
|
|
||||||
confirmClockRunning(true);
|
confirmResumed();
|
||||||
confirmPauseOverlayShown(false);
|
AddAssert("exited", () => !Player.IsCurrentScreen());
|
||||||
|
|
||||||
AddAssert("not exited", () => Player.IsCurrentScreen());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -131,22 +140,37 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
|
|
||||||
confirmClockRunning(false);
|
confirmClockRunning(false);
|
||||||
|
|
||||||
pause();
|
AddStep("pause via forced pause", () => Player.Pause());
|
||||||
|
|
||||||
confirmClockRunning(false);
|
|
||||||
confirmPauseOverlayShown(false);
|
|
||||||
|
|
||||||
|
confirmPausedWithNoOverlay();
|
||||||
AddAssert("fail overlay still shown", () => Player.FailOverlayVisible);
|
AddAssert("fail overlay still shown", () => Player.FailOverlayVisible);
|
||||||
|
|
||||||
exitAndConfirm();
|
exitAndConfirm();
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestExitFromFailedGameplay()
|
public void TestExitFromFailedGameplayAfterFailAnimation()
|
||||||
{
|
{
|
||||||
AddUntilStep("wait for fail", () => Player.HasFailed);
|
AddUntilStep("wait for fail", () => Player.HasFailed);
|
||||||
AddStep("exit", () => Player.Exit());
|
AddUntilStep("wait for fail overlay shown", () => Player.FailOverlayVisible);
|
||||||
|
|
||||||
|
confirmClockRunning(false);
|
||||||
|
|
||||||
|
AddStep("exit via user pause", () => Player.ExitViaPause());
|
||||||
|
confirmExited();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestExitFromFailedGameplayDuringFailAnimation()
|
||||||
|
{
|
||||||
|
AddUntilStep("wait for fail", () => Player.HasFailed);
|
||||||
|
|
||||||
|
// will finish the fail animation and show the fail/pause screen.
|
||||||
|
AddStep("attempt exit via pause key", () => Player.ExitViaPause());
|
||||||
|
AddAssert("fail overlay shown", () => Player.FailOverlayVisible);
|
||||||
|
|
||||||
|
// will actually exit.
|
||||||
|
AddStep("exit via pause key", () => Player.ExitViaPause());
|
||||||
confirmExited();
|
confirmExited();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -245,7 +269,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
|
|
||||||
private void pauseAndConfirm()
|
private void pauseAndConfirm()
|
||||||
{
|
{
|
||||||
pause();
|
pauseFromUserExitKey();
|
||||||
confirmPaused();
|
confirmPaused();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -257,7 +281,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
|
|
||||||
private void exitAndConfirm()
|
private void exitAndConfirm()
|
||||||
{
|
{
|
||||||
AddUntilStep("player not exited", () => Player.IsCurrentScreen());
|
confirmNotExited();
|
||||||
AddStep("exit", () => Player.Exit());
|
AddStep("exit", () => Player.Exit());
|
||||||
confirmExited();
|
confirmExited();
|
||||||
confirmNoTrackAdjustments();
|
confirmNoTrackAdjustments();
|
||||||
@ -266,7 +290,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
private void confirmPaused()
|
private void confirmPaused()
|
||||||
{
|
{
|
||||||
confirmClockRunning(false);
|
confirmClockRunning(false);
|
||||||
AddAssert("player not exited", () => Player.IsCurrentScreen());
|
confirmNotExited();
|
||||||
AddAssert("player not failed", () => !Player.HasFailed);
|
AddAssert("player not failed", () => !Player.HasFailed);
|
||||||
AddAssert("pause overlay shown", () => Player.PauseOverlayVisible);
|
AddAssert("pause overlay shown", () => Player.PauseOverlayVisible);
|
||||||
}
|
}
|
||||||
@ -277,18 +301,22 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
confirmPauseOverlayShown(false);
|
confirmPauseOverlayShown(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void confirmExited()
|
private void confirmPausedWithNoOverlay()
|
||||||
{
|
{
|
||||||
AddUntilStep("player exited", () => !Player.IsCurrentScreen());
|
confirmClockRunning(false);
|
||||||
|
confirmPauseOverlayShown(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void confirmExited() => AddUntilStep("player exited", () => !Player.IsCurrentScreen());
|
||||||
|
private void confirmNotExited() => AddAssert("player not exited", () => Player.IsCurrentScreen());
|
||||||
|
|
||||||
private void confirmNoTrackAdjustments()
|
private void confirmNoTrackAdjustments()
|
||||||
{
|
{
|
||||||
AddAssert("track has no adjustments", () => Beatmap.Value.Track.AggregateFrequency.Value == 1);
|
AddAssert("track has no adjustments", () => Beatmap.Value.Track.AggregateFrequency.Value == 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void restart() => AddStep("restart", () => Player.Restart());
|
private void restart() => AddStep("restart", () => Player.Restart());
|
||||||
private void pause() => AddStep("pause", () => Player.Pause());
|
private void pauseFromUserExitKey() => AddStep("user pause", () => Player.ExitViaPause());
|
||||||
private void resume() => AddStep("resume", () => Player.Resume());
|
private void resume() => AddStep("resume", () => Player.Resume());
|
||||||
|
|
||||||
private void confirmPauseOverlayShown(bool isShown) =>
|
private void confirmPauseOverlayShown(bool isShown) =>
|
||||||
@ -307,6 +335,10 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
|
|
||||||
public bool PauseOverlayVisible => PauseOverlay.State.Value == Visibility.Visible;
|
public bool PauseOverlayVisible => PauseOverlay.State.Value == Visibility.Visible;
|
||||||
|
|
||||||
|
public void ExitViaPause() => PerformExit(true);
|
||||||
|
|
||||||
|
public void ExitViaQuickExit() => PerformExit(false);
|
||||||
|
|
||||||
public override void OnEntering(IScreen last)
|
public override void OnEntering(IScreen last)
|
||||||
{
|
{
|
||||||
base.OnEntering(last);
|
base.OnEntering(last);
|
||||||
|
@ -17,6 +17,7 @@ using osu.Game.Graphics.UserInterface;
|
|||||||
using osu.Game.Online.API;
|
using osu.Game.Online.API;
|
||||||
using osu.Game.Overlays;
|
using osu.Game.Overlays;
|
||||||
using osu.Game.Rulesets;
|
using osu.Game.Rulesets;
|
||||||
|
using osu.Game.Rulesets.Mods;
|
||||||
using osu.Game.Scoring;
|
using osu.Game.Scoring;
|
||||||
using osu.Game.Screens;
|
using osu.Game.Screens;
|
||||||
using osu.Game.Screens.Menu;
|
using osu.Game.Screens.Menu;
|
||||||
@ -115,6 +116,8 @@ namespace osu.Game.Tests.Visual.Navigation
|
|||||||
|
|
||||||
public new Bindable<RulesetInfo> Ruleset => base.Ruleset;
|
public new Bindable<RulesetInfo> Ruleset => base.Ruleset;
|
||||||
|
|
||||||
|
public new Bindable<IReadOnlyList<Mod>> SelectedMods => base.SelectedMods;
|
||||||
|
|
||||||
// if we don't do this, when running under nUnit the version that gets populated is that of nUnit.
|
// if we don't do this, when running under nUnit the version that gets populated is that of nUnit.
|
||||||
public override string Version => "test game";
|
public override string Version => "test game";
|
||||||
|
|
||||||
|
@ -11,7 +11,9 @@ using osu.Game.Beatmaps;
|
|||||||
using osu.Game.Overlays;
|
using osu.Game.Overlays;
|
||||||
using osu.Game.Overlays.Mods;
|
using osu.Game.Overlays.Mods;
|
||||||
using osu.Game.Overlays.Toolbar;
|
using osu.Game.Overlays.Toolbar;
|
||||||
|
using osu.Game.Rulesets.Osu.Mods;
|
||||||
using osu.Game.Screens.Play;
|
using osu.Game.Screens.Play;
|
||||||
|
using osu.Game.Screens.Ranking;
|
||||||
using osu.Game.Screens.Select;
|
using osu.Game.Screens.Select;
|
||||||
using osu.Game.Screens.Select.Options;
|
using osu.Game.Screens.Select.Options;
|
||||||
using osu.Game.Tests.Beatmaps.IO;
|
using osu.Game.Tests.Beatmaps.IO;
|
||||||
@ -41,6 +43,30 @@ namespace osu.Game.Tests.Visual.Navigation
|
|||||||
exitViaEscapeAndConfirm();
|
exitViaEscapeAndConfirm();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestRetryFromResults()
|
||||||
|
{
|
||||||
|
Player player = null;
|
||||||
|
ResultsScreen results = null;
|
||||||
|
|
||||||
|
WorkingBeatmap beatmap() => Game.Beatmap.Value;
|
||||||
|
|
||||||
|
PushAndConfirm(() => new TestSongSelect());
|
||||||
|
|
||||||
|
AddStep("import beatmap", () => ImportBeatmapTest.LoadOszIntoOsu(Game, virtualTrack: true).Wait());
|
||||||
|
|
||||||
|
AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault);
|
||||||
|
|
||||||
|
AddStep("set autoplay", () => Game.SelectedMods.Value = new[] { new OsuModAutoplay() });
|
||||||
|
|
||||||
|
AddStep("press enter", () => InputManager.Key(Key.Enter));
|
||||||
|
AddUntilStep("wait for player", () => (player = Game.ScreenStack.CurrentScreen as Player) != null);
|
||||||
|
AddStep("seek to end", () => beatmap().Track.Seek(beatmap().Track.Length));
|
||||||
|
AddUntilStep("wait for pass", () => (results = Game.ScreenStack.CurrentScreen as ResultsScreen) != null && results.IsLoaded);
|
||||||
|
AddStep("attempt to retry", () => results.ChildrenOfType<HotkeyRetryOverlay>().First().Action());
|
||||||
|
AddUntilStep("wait for player", () => Game.ScreenStack.CurrentScreen != player && Game.ScreenStack.CurrentScreen is Player);
|
||||||
|
}
|
||||||
|
|
||||||
[TestCase(true)]
|
[TestCase(true)]
|
||||||
[TestCase(false)]
|
[TestCase(false)]
|
||||||
public void TestSongContinuesAfterExitPlayer(bool withUserPause)
|
public void TestSongContinuesAfterExitPlayer(bool withUserPause)
|
||||||
|
@ -339,7 +339,7 @@ namespace osu.Game.Screens.Play
|
|||||||
{
|
{
|
||||||
HoldToQuit =
|
HoldToQuit =
|
||||||
{
|
{
|
||||||
Action = performUserRequestedExit,
|
Action = () => PerformExit(true),
|
||||||
IsPaused = { BindTarget = GameplayClockContainer.IsPaused }
|
IsPaused = { BindTarget = GameplayClockContainer.IsPaused }
|
||||||
},
|
},
|
||||||
PlayerSettingsOverlay = { PlaybackSettings = { UserPlaybackRate = { BindTarget = GameplayClockContainer.UserPlaybackRate } } },
|
PlayerSettingsOverlay = { PlaybackSettings = { UserPlaybackRate = { BindTarget = GameplayClockContainer.UserPlaybackRate } } },
|
||||||
@ -363,14 +363,14 @@ namespace osu.Game.Screens.Play
|
|||||||
FailOverlay = new FailOverlay
|
FailOverlay = new FailOverlay
|
||||||
{
|
{
|
||||||
OnRetry = Restart,
|
OnRetry = Restart,
|
||||||
OnQuit = performUserRequestedExit,
|
OnQuit = () => PerformExit(true),
|
||||||
},
|
},
|
||||||
PauseOverlay = new PauseOverlay
|
PauseOverlay = new PauseOverlay
|
||||||
{
|
{
|
||||||
OnResume = Resume,
|
OnResume = Resume,
|
||||||
Retries = RestartCount,
|
Retries = RestartCount,
|
||||||
OnRetry = Restart,
|
OnRetry = Restart,
|
||||||
OnQuit = performUserRequestedExit,
|
OnQuit = () => PerformExit(true),
|
||||||
},
|
},
|
||||||
new HotkeyExitOverlay
|
new HotkeyExitOverlay
|
||||||
{
|
{
|
||||||
@ -379,7 +379,7 @@ namespace osu.Game.Screens.Play
|
|||||||
if (!this.IsCurrentScreen()) return;
|
if (!this.IsCurrentScreen()) return;
|
||||||
|
|
||||||
fadeOut(true);
|
fadeOut(true);
|
||||||
PerformExit(true);
|
PerformExit(false);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
failAnimation = new FailAnimation(DrawableRuleset) { OnComplete = onFailComplete, },
|
failAnimation = new FailAnimation(DrawableRuleset) { OnComplete = onFailComplete, },
|
||||||
@ -478,23 +478,43 @@ namespace osu.Game.Screens.Play
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Exits the <see cref="Player"/>.
|
/// Exits the <see cref="Player"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="userRequested">
|
/// <param name="showDialogFirst">
|
||||||
/// Whether the exit is requested by the user, or a higher-level game component.
|
/// Whether the pause or fail dialog should be shown before performing an exit.
|
||||||
/// Pausing is allowed only in the former case.
|
/// If true and a dialog is not yet displayed, the exit will be blocked the the relevant dialog will display instead.
|
||||||
/// </param>
|
/// </param>
|
||||||
protected void PerformExit(bool userRequested)
|
protected void PerformExit(bool showDialogFirst)
|
||||||
{
|
{
|
||||||
// if a restart has been requested, cancel any pending completion (user has shown intent to restart).
|
// if a restart has been requested, cancel any pending completion (user has shown intent to restart).
|
||||||
completionProgressDelegate?.Cancel();
|
completionProgressDelegate?.Cancel();
|
||||||
|
|
||||||
ValidForResume = false;
|
// there is a chance that the exit was performed after the transition to results has started.
|
||||||
|
// we want to give the user what they want, so forcefully return to this screen (to proceed with the upwards exit process).
|
||||||
|
if (!this.IsCurrentScreen())
|
||||||
|
{
|
||||||
|
ValidForResume = false;
|
||||||
|
this.MakeCurrent();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!this.IsCurrentScreen()) return;
|
bool pauseOrFailDialogVisible =
|
||||||
|
PauseOverlay.State.Value == Visibility.Visible || FailOverlay.State.Value == Visibility.Visible;
|
||||||
|
|
||||||
if (userRequested)
|
if (showDialogFirst && !pauseOrFailDialogVisible)
|
||||||
performUserRequestedExit();
|
{
|
||||||
else
|
// if the fail animation is currently in progress, accelerate it (it will show the pause dialog on completion).
|
||||||
this.Exit();
|
if (ValidForResume && HasFailed)
|
||||||
|
{
|
||||||
|
failAnimation.FinishTransforms(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// in the case a dialog needs to be shown, attempt to pause and show it.
|
||||||
|
// this may fail (see internal checks in Pause()) at which point the exit attempt will be aborted.
|
||||||
|
Pause();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.Exit();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void performUserRequestedSkip()
|
private void performUserRequestedSkip()
|
||||||
@ -508,20 +528,6 @@ namespace osu.Game.Screens.Play
|
|||||||
updateSampleDisabledState();
|
updateSampleDisabledState();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void performUserRequestedExit()
|
|
||||||
{
|
|
||||||
if (ValidForResume && HasFailed && !FailOverlay.IsPresent)
|
|
||||||
{
|
|
||||||
failAnimation.FinishTransforms(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (canPause)
|
|
||||||
Pause();
|
|
||||||
else
|
|
||||||
this.Exit();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Restart gameplay via a parent <see cref="PlayerLoader"/>.
|
/// Restart gameplay via a parent <see cref="PlayerLoader"/>.
|
||||||
/// <remarks>This can be called from a child screen in order to trigger the restart process.</remarks>
|
/// <remarks>This can be called from a child screen in order to trigger the restart process.</remarks>
|
||||||
@ -538,10 +544,7 @@ namespace osu.Game.Screens.Play
|
|||||||
sampleRestart?.Play();
|
sampleRestart?.Play();
|
||||||
RestartRequested?.Invoke();
|
RestartRequested?.Invoke();
|
||||||
|
|
||||||
if (this.IsCurrentScreen())
|
PerformExit(false);
|
||||||
PerformExit(true);
|
|
||||||
else
|
|
||||||
this.MakeCurrent();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private ScheduledDelegate completionProgressDelegate;
|
private ScheduledDelegate completionProgressDelegate;
|
||||||
@ -809,14 +812,6 @@ namespace osu.Game.Screens.Play
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ValidForResume is false when restarting
|
|
||||||
if (ValidForResume)
|
|
||||||
{
|
|
||||||
if (pauseCooldownActive && !GameplayClockContainer.IsPaused.Value)
|
|
||||||
// still want to block if we are within the cooldown period and not already paused.
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// GameplayClockContainer performs seeks / start / stop operations on the beatmap's track.
|
// 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.
|
// as we are no longer the current screen, we cannot guarantee the track is still usable.
|
||||||
GameplayClockContainer?.StopUsingBeatmapClock();
|
GameplayClockContainer?.StopUsingBeatmapClock();
|
||||||
|
Loading…
Reference in New Issue
Block a user