diff --git a/osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs b/osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs index d90d3d26eb..b045b82960 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs @@ -3,6 +3,7 @@ using System; using osu.Framework.Allocation; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; @@ -35,9 +36,11 @@ private void load() { OsuResumeOverlayInputBlocker? inputBlocker = null; - if (drawableRuleset != null) + var drawableOsuRuleset = (DrawableOsuRuleset?)drawableRuleset; + + if (drawableOsuRuleset != null) { - var osuPlayfield = (OsuPlayfield)drawableRuleset.Playfield; + var osuPlayfield = drawableOsuRuleset.Playfield; osuPlayfield.AttachResumeOverlayInputBlocker(inputBlocker = new OsuResumeOverlayInputBlocker()); } @@ -45,13 +48,14 @@ private void load() { Child = clickToResumeCursor = new OsuClickToResumeCursor { - ResumeRequested = () => + ResumeRequested = action => { // since the user had to press a button to tap the resume cursor, // block that press event from potentially reaching a hit circle that's behind the cursor. // we cannot do this from OsuClickToResumeCursor directly since we're in a different input manager tree than the gameplay one, // so we rely on a dedicated input blocking component that's implanted in there to do that for us. - if (inputBlocker != null) + // note this only matters when the user didn't pause while they were holding the same key that they are resuming with. + if (inputBlocker != null && !drawableOsuRuleset.AsNonNull().KeyBindingInputManager.PressedActions.Contains(action)) inputBlocker.BlockNextPress = true; Resume(); @@ -94,7 +98,7 @@ public partial class OsuClickToResumeCursor : OsuCursor, IKeyBindingHandler true; - public Action? ResumeRequested; + public Action? ResumeRequested; private Container scaleTransitionContainer = null!; public OsuClickToResumeCursor() @@ -136,7 +140,7 @@ public bool OnPressed(KeyBindingPressEvent e) return false; scaleTransitionContainer.ScaleTo(2, TRANSITION_TIME, Easing.OutQuint); - ResumeRequested?.Invoke(); + ResumeRequested?.Invoke(e.Action); return true; } diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePauseInputHandling.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePauseInputHandling.cs index 2c58e64831..2c0d7d6744 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePauseInputHandling.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePauseInputHandling.cs @@ -47,6 +47,16 @@ public partial class TestScenePauseInputHandling : PlayerTestScene { Position = OsuPlayfield.BASE_SIZE / 2, StartTime = 5000, + }, + new HitCircle + { + Position = OsuPlayfield.BASE_SIZE / 2, + StartTime = 10000, + }, + new HitCircle + { + Position = OsuPlayfield.BASE_SIZE / 2, + StartTime = 15000, } } }; @@ -256,7 +266,7 @@ public void TestManiaHeldInputRemainHeldAfterResume() } [Test] - public void TestOsuRegisterInputFromPressingOrangeCursorButPressIsBlocked() + public void TestOsuHitCircleNotReceivingInputOnResume() { KeyCounter counter = null!; @@ -281,6 +291,64 @@ public void TestOsuRegisterInputFromPressingOrangeCursorButPressIsBlocked() AddAssert("button is released in kbc", () => !Player.DrawableRuleset.Playfield.FindClosestParent()!.PressedActions.Any()); } + [Test] + public void TestOsuHitCircleNotReceivingInputOnResume_PauseWhileHoldingSameKey() + { + KeyCounter counter = null!; + + loadPlayer(() => new OsuRuleset()); + AddStep("get key counter", () => counter = this.ChildrenOfType().Single(k => k.Trigger is KeyCounterActionTrigger actionTrigger && actionTrigger.Action == OsuAction.LeftButton)); + + AddStep("press Z", () => InputManager.PressKey(Key.Z)); + AddAssert("circle hit", () => Player.ScoreProcessor.HighestCombo.Value, () => Is.EqualTo(1)); + + AddStep("pause", () => Player.Pause()); + AddStep("release Z", () => InputManager.ReleaseKey(Key.Z)); + + AddStep("resume", () => Player.Resume()); + AddStep("go to resume cursor", () => InputManager.MoveMouseTo(this.ChildrenOfType().Single())); + AddStep("press Z to resume", () => InputManager.PressKey(Key.Z)); + AddStep("release Z", () => InputManager.ReleaseKey(Key.Z)); + + checkKey(() => counter, 1, false); + + seekTo(5000); + + AddStep("press Z", () => InputManager.PressKey(Key.Z)); + + checkKey(() => counter, 2, true); + AddAssert("circle hit", () => Player.ScoreProcessor.HighestCombo.Value, () => Is.EqualTo(2)); + + AddStep("release Z", () => InputManager.ReleaseKey(Key.Z)); + checkKey(() => counter, 2, false); + } + + [Test] + public void TestOsuHitCircleNotReceivingInputOnResume_PauseWhileHoldingOtherKey() + { + loadPlayer(() => new OsuRuleset()); + + AddStep("press X", () => InputManager.PressKey(Key.X)); + AddAssert("circle hit", () => Player.ScoreProcessor.HighestCombo.Value, () => Is.EqualTo(1)); + + seekTo(5000); + + AddStep("pause", () => Player.Pause()); + AddStep("release X", () => InputManager.ReleaseKey(Key.X)); + + AddStep("resume", () => Player.Resume()); + AddStep("go to resume cursor", () => InputManager.MoveMouseTo(this.ChildrenOfType().Single())); + AddStep("press Z to resume", () => InputManager.PressKey(Key.Z)); + AddStep("release Z", () => InputManager.ReleaseKey(Key.Z)); + + AddAssert("circle not hit", () => Player.ScoreProcessor.HighestCombo.Value, () => Is.EqualTo(1)); + + AddStep("press X", () => InputManager.PressKey(Key.X)); + AddStep("release X", () => InputManager.ReleaseKey(Key.X)); + + AddAssert("circle hit", () => Player.ScoreProcessor.HighestCombo.Value, () => Is.EqualTo(2)); + } + private void loadPlayer(Func createRuleset) { AddStep("set ruleset", () => currentRuleset = createRuleset()); @@ -288,12 +356,17 @@ private void loadPlayer(Func createRuleset) AddUntilStep("player loaded", () => Player.IsLoaded && Player.Alpha == 1); AddUntilStep("wait for hud", () => Player.HUDOverlay.ChildrenOfType().All(s => s.ComponentsLoaded)); - AddStep("seek to gameplay", () => Player.GameplayClockContainer.Seek(0)); - AddUntilStep("wait for seek to finish", () => Player.DrawableRuleset.FrameStableClock.CurrentTime, () => Is.EqualTo(0).Within(500)); + seekTo(0); AddAssert("not in break", () => !Player.IsBreakTime.Value); AddStep("move cursor to center", () => InputManager.MoveMouseTo(Player.DrawableRuleset.Playfield)); } + private void seekTo(double time) + { + AddStep($"seek to {time}ms", () => Player.GameplayClockContainer.Seek(time)); + AddUntilStep("wait for seek to finish", () => Player.DrawableRuleset.FrameStableClock.CurrentTime, () => Is.EqualTo(time).Within(500)); + } + private void checkKey(Func counter, int count, bool active) { AddAssert($"key count = {count}", () => counter().CountPresses.Value, () => Is.EqualTo(count));