1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-15 14:02:55 +08:00

Merge pull request #8800 from iiSaLMaN/allow-cancelling-completion

Fix results screen pushed after rewinding in-between push delay
This commit is contained in:
Dean Herbert 2020-04-21 12:49:49 +09:00 committed by GitHub
commit af32f51116
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 175 additions and 43 deletions

View File

@ -27,7 +27,6 @@ namespace osu.Game.Rulesets.Mania.Tests
private const double time_after_tail = 5250; private const double time_after_tail = 5250;
private List<JudgementResult> judgementResults; private List<JudgementResult> judgementResults;
private bool allJudgedFired;
/// <summary> /// <summary>
/// -----[ ]----- /// -----[ ]-----
@ -283,20 +282,15 @@ namespace osu.Game.Rulesets.Mania.Tests
{ {
if (currentPlayer == p) judgementResults.Add(result); if (currentPlayer == p) judgementResults.Add(result);
}; };
p.ScoreProcessor.AllJudged += () =>
{
if (currentPlayer == p) allJudgedFired = true;
};
}; };
LoadScreen(currentPlayer = p); LoadScreen(currentPlayer = p);
allJudgedFired = false;
judgementResults = new List<JudgementResult>(); judgementResults = new List<JudgementResult>();
}); });
AddUntilStep("Beatmap at 0", () => Beatmap.Value.Track.CurrentTime == 0); AddUntilStep("Beatmap at 0", () => Beatmap.Value.Track.CurrentTime == 0);
AddUntilStep("Wait until player is loaded", () => currentPlayer.IsCurrentScreen()); AddUntilStep("Wait until player is loaded", () => currentPlayer.IsCurrentScreen());
AddUntilStep("Wait for all judged", () => allJudgedFired); AddUntilStep("Wait for completion", () => currentPlayer.ScoreProcessor.HasCompleted.Value);
} }
private class ScoreAccessibleReplayPlayer : ReplayPlayer private class ScoreAccessibleReplayPlayer : ReplayPlayer

View File

@ -354,7 +354,6 @@ namespace osu.Game.Rulesets.Osu.Tests
private ScoreAccessibleReplayPlayer currentPlayer; private ScoreAccessibleReplayPlayer currentPlayer;
private List<JudgementResult> judgementResults; private List<JudgementResult> judgementResults;
private bool allJudgedFired;
private void performTest(List<OsuHitObject> hitObjects, List<ReplayFrame> frames) private void performTest(List<OsuHitObject> hitObjects, List<ReplayFrame> frames)
{ {
@ -380,20 +379,15 @@ namespace osu.Game.Rulesets.Osu.Tests
{ {
if (currentPlayer == p) judgementResults.Add(result); if (currentPlayer == p) judgementResults.Add(result);
}; };
p.ScoreProcessor.AllJudged += () =>
{
if (currentPlayer == p) allJudgedFired = true;
};
}; };
LoadScreen(currentPlayer = p); LoadScreen(currentPlayer = p);
allJudgedFired = false;
judgementResults = new List<JudgementResult>(); judgementResults = new List<JudgementResult>();
}); });
AddUntilStep("Beatmap at 0", () => Beatmap.Value.Track.CurrentTime == 0); AddUntilStep("Beatmap at 0", () => Beatmap.Value.Track.CurrentTime == 0);
AddUntilStep("Wait until player is loaded", () => currentPlayer.IsCurrentScreen()); AddUntilStep("Wait until player is loaded", () => currentPlayer.IsCurrentScreen());
AddUntilStep("Wait for all judged", () => allJudgedFired); AddUntilStep("Wait for completion", () => currentPlayer.ScoreProcessor.HasCompleted.Value);
} }
private class TestHitCircle : HitCircle private class TestHitCircle : HitCircle

View File

@ -47,7 +47,6 @@ namespace osu.Game.Rulesets.Osu.Tests
private const double time_slider_end = 4000; private const double time_slider_end = 4000;
private List<JudgementResult> judgementResults; private List<JudgementResult> judgementResults;
private bool allJudgedFired;
/// <summary> /// <summary>
/// Scenario: /// Scenario:
@ -375,20 +374,15 @@ namespace osu.Game.Rulesets.Osu.Tests
{ {
if (currentPlayer == p) judgementResults.Add(result); if (currentPlayer == p) judgementResults.Add(result);
}; };
p.ScoreProcessor.AllJudged += () =>
{
if (currentPlayer == p) allJudgedFired = true;
};
}; };
LoadScreen(currentPlayer = p); LoadScreen(currentPlayer = p);
allJudgedFired = false;
judgementResults = new List<JudgementResult>(); judgementResults = new List<JudgementResult>();
}); });
AddUntilStep("Beatmap at 0", () => Beatmap.Value.Track.CurrentTime == 0); AddUntilStep("Beatmap at 0", () => Beatmap.Value.Track.CurrentTime == 0);
AddUntilStep("Wait until player is loaded", () => currentPlayer.IsCurrentScreen()); AddUntilStep("Wait until player is loaded", () => currentPlayer.IsCurrentScreen());
AddUntilStep("Wait for all judged", () => allJudgedFired); AddUntilStep("Wait for completion", () => currentPlayer.ScoreProcessor.HasCompleted.Value);
} }
private class ScoreAccessibleReplayPlayer : ReplayPlayer private class ScoreAccessibleReplayPlayer : ReplayPlayer

View File

@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
[Test] [Test]
public void TestZeroTickTimeOffsets() public void TestZeroTickTimeOffsets()
{ {
AddUntilStep("gameplay finished", () => Player.ScoreProcessor.HasCompleted); AddUntilStep("gameplay finished", () => Player.ScoreProcessor.HasCompleted.Value);
AddAssert("all tick offsets are 0", () => Player.Results.Where(r => r.HitObject is SwellTick).All(r => r.TimeOffset == 0)); AddAssert("all tick offsets are 0", () => Player.Results.Where(r => r.HitObject is SwellTick).All(r => r.TimeOffset == 0));
} }

View File

@ -0,0 +1,139 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Track;
using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Framework.Timing;
using osu.Game.Beatmaps;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Storyboards;
using osuTK;
namespace osu.Game.Tests.Visual.Gameplay
{
public class TestSceneCompletionCancellation : PlayerTestScene
{
private Track track;
[Resolved]
private AudioManager audio { get; set; }
private int resultsDisplayWaitCount =>
(int)((Screens.Play.Player.RESULTS_DISPLAY_DELAY / TimePerAction) * 2);
protected override bool AllowFail => false;
public TestSceneCompletionCancellation()
: base(new OsuRuleset())
{
}
[SetUpSteps]
public override void SetUpSteps()
{
base.SetUpSteps();
// Ensure track has actually running before attempting to seek
AddUntilStep("wait for track to start running", () => track.IsRunning);
}
[Test]
public void TestCancelCompletionOnRewind()
{
complete();
cancel();
checkNoRanking();
}
[Test]
public void TestReCompleteAfterCancellation()
{
complete();
cancel();
complete();
AddUntilStep("attempted to push ranking", () => ((FakeRankingPushPlayer)Player).GotoRankingInvoked);
}
/// <summary>
/// Tests whether can still pause after cancelling completion by reverting <see cref="IScreen.ValidForResume"/> back to true.
/// </summary>
[Test]
public void TestCanPauseAfterCancellation()
{
complete();
cancel();
AddStep("pause", () => Player.Pause());
AddAssert("paused successfully", () => Player.GameplayClockContainer.IsPaused.Value);
checkNoRanking();
}
private void complete()
{
AddStep("seek to completion", () => track.Seek(5000));
AddUntilStep("completion set by processor", () => Player.ScoreProcessor.HasCompleted.Value);
}
private void cancel()
{
AddStep("rewind to cancel", () => track.Seek(4000));
AddUntilStep("completion cleared by processor", () => !Player.ScoreProcessor.HasCompleted.Value);
}
private void checkNoRanking()
{
// wait to ensure there was no attempt of pushing the results screen.
AddWaitStep("wait", resultsDisplayWaitCount);
AddAssert("no attempt to push ranking", () => !((FakeRankingPushPlayer)Player).GotoRankingInvoked);
}
protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null)
{
var working = new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audio);
track = working.Track;
return working;
}
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset)
{
var beatmap = new Beatmap();
for (int i = 1; i <= 19; i++)
{
beatmap.HitObjects.Add(new HitCircle
{
Position = new Vector2(256, 192),
StartTime = i * 250,
});
}
return beatmap;
}
protected override TestPlayer CreatePlayer(Ruleset ruleset) => new FakeRankingPushPlayer();
public class FakeRankingPushPlayer : TestPlayer
{
public bool GotoRankingInvoked;
public FakeRankingPushPlayer()
: base(true, true)
{
}
protected override void GotoRanking()
{
GotoRankingInvoked = true;
}
}
}
}

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using osu.Framework.Bindables;
using osu.Framework.Extensions.TypeExtensions; using osu.Framework.Extensions.TypeExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
@ -12,11 +13,6 @@ namespace osu.Game.Rulesets.Scoring
{ {
public abstract class JudgementProcessor : Component public abstract class JudgementProcessor : Component
{ {
/// <summary>
/// Invoked when all <see cref="HitObject"/>s have been judged by this <see cref="JudgementProcessor"/>.
/// </summary>
public event Action AllJudged;
/// <summary> /// <summary>
/// Invoked when a new judgement has occurred. This occurs after the judgement has been processed by this <see cref="JudgementProcessor"/>. /// Invoked when a new judgement has occurred. This occurs after the judgement has been processed by this <see cref="JudgementProcessor"/>.
/// </summary> /// </summary>
@ -32,10 +28,12 @@ namespace osu.Game.Rulesets.Scoring
/// </summary> /// </summary>
public int JudgedHits { get; private set; } public int JudgedHits { get; private set; }
private readonly BindableBool hasCompleted = new BindableBool();
/// <summary> /// <summary>
/// Whether all <see cref="Judgement"/>s have been processed. /// Whether all <see cref="Judgement"/>s have been processed.
/// </summary> /// </summary>
public bool HasCompleted => JudgedHits == MaxHits; public IBindable<bool> HasCompleted => hasCompleted;
/// <summary> /// <summary>
/// Applies a <see cref="IBeatmap"/> to this <see cref="ScoreProcessor"/>. /// Applies a <see cref="IBeatmap"/> to this <see cref="ScoreProcessor"/>.
@ -60,8 +58,7 @@ namespace osu.Game.Rulesets.Scoring
NewJudgement?.Invoke(result); NewJudgement?.Invoke(result);
if (HasCompleted) updateHasCompleted();
AllJudged?.Invoke();
} }
/// <summary> /// <summary>
@ -72,6 +69,8 @@ namespace osu.Game.Rulesets.Scoring
{ {
JudgedHits--; JudgedHits--;
updateHasCompleted();
RevertResultInternal(result); RevertResultInternal(result);
} }
@ -134,5 +133,7 @@ namespace osu.Game.Rulesets.Scoring
ApplyResult(result); ApplyResult(result);
} }
} }
private void updateHasCompleted() => hasCompleted.Value = JudgedHits == MaxHits;
} }
} }

View File

@ -51,7 +51,7 @@ namespace osu.Game.Screens.Play
isBreakTime.Value = getCurrentBreak()?.HasEffect == true isBreakTime.Value = getCurrentBreak()?.HasEffect == true
|| Clock.CurrentTime < gameplayStartTime || Clock.CurrentTime < gameplayStartTime
|| scoreProcessor?.HasCompleted == true; || scoreProcessor?.HasCompleted.Value == true;
} }
private BreakPeriod getCurrentBreak() private BreakPeriod getCurrentBreak()

View File

@ -37,6 +37,11 @@ namespace osu.Game.Screens.Play
[Cached] [Cached]
public class Player : ScreenWithBeatmapBackground public class Player : ScreenWithBeatmapBackground
{ {
/// <summary>
/// The delay upon completion of the beatmap before displaying the results screen.
/// </summary>
public const double RESULTS_DISPLAY_DELAY = 1000.0;
public override bool AllowBackButton => false; // handled by HoldForMenuButton public override bool AllowBackButton => false; // handled by HoldForMenuButton
protected override UserActivity InitialActivity => new UserActivity.SoloGame(Beatmap.Value.BeatmapInfo, Ruleset.Value); protected override UserActivity InitialActivity => new UserActivity.SoloGame(Beatmap.Value.BeatmapInfo, Ruleset.Value);
@ -197,7 +202,7 @@ namespace osu.Game.Screens.Play
}; };
// Bind the judgement processors to ourselves // Bind the judgement processors to ourselves
ScoreProcessor.AllJudged += onCompletion; ScoreProcessor.HasCompleted.ValueChanged += updateCompletionState;
HealthProcessor.Failed += onFail; HealthProcessor.Failed += onFail;
foreach (var mod in Mods.Value.OfType<IApplicableToScoreProcessor>()) foreach (var mod in Mods.Value.OfType<IApplicableToScoreProcessor>())
@ -412,22 +417,33 @@ namespace osu.Game.Screens.Play
private ScheduledDelegate completionProgressDelegate; private ScheduledDelegate completionProgressDelegate;
private void onCompletion() private void updateCompletionState(ValueChangedEvent<bool> completionState)
{ {
// screen may be in the exiting transition phase. // screen may be in the exiting transition phase.
if (!this.IsCurrentScreen()) if (!this.IsCurrentScreen())
return; return;
if (!completionState.NewValue)
{
completionProgressDelegate?.Cancel();
completionProgressDelegate = null;
ValidForResume = true;
return;
}
if (completionProgressDelegate != null)
throw new InvalidOperationException($"{nameof(updateCompletionState)} was fired more than once");
// Only show the completion screen if the player hasn't failed // Only show the completion screen if the player hasn't failed
if (HealthProcessor.HasFailed || completionProgressDelegate != null) if (HealthProcessor.HasFailed)
return; return;
ValidForResume = false; ValidForResume = false;
if (!showResults) return; if (!showResults) return;
using (BeginDelayedSequence(1000)) using (BeginDelayedSequence(RESULTS_DISPLAY_DELAY))
scheduleGotoRanking(); completionProgressDelegate = Schedule(GotoRanking);
} }
protected virtual ScoreInfo CreateScore() protected virtual ScoreInfo CreateScore()
@ -679,12 +695,6 @@ namespace osu.Game.Screens.Play
storyboardReplacesBackground.Value = false; storyboardReplacesBackground.Value = false;
} }
private void scheduleGotoRanking()
{
completionProgressDelegate?.Cancel();
completionProgressDelegate = Schedule(GotoRanking);
}
#endregion #endregion
} }
} }

View File

@ -46,7 +46,7 @@ namespace osu.Game.Tests.Visual
public bool CheckFailed(bool failed) public bool CheckFailed(bool failed)
{ {
if (!failed) if (!failed)
return ScoreProcessor.HasCompleted && !HealthProcessor.HasFailed; return ScoreProcessor.HasCompleted.Value && !HealthProcessor.HasFailed;
return HealthProcessor.HasFailed; return HealthProcessor.HasFailed;
} }