Skip to content

Commit

Permalink
Merge pull request #29250 from frenzibyte/fix-pause-in-osu-again
Browse files Browse the repository at this point in the history
Fix pause input handling in osu! not working like stable
  • Loading branch information
smoogipoo authored Aug 5, 2024
2 parents 1fd1d8a + 20b8905 commit 95aab6c
Show file tree
Hide file tree
Showing 6 changed files with 132 additions and 35 deletions.
2 changes: 1 addition & 1 deletion osu.Android.props
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Framework.Android" Version="2024.720.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2024.802.0" />
</ItemGroup>
<PropertyGroup>
<!-- Fody does not handle Android build well, and warns when unchanged.
Expand Down
9 changes: 9 additions & 0 deletions osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,15 @@ private void onNewResult(DrawableHitObject judgedObject, JudgementResult result)

public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => HitObjectContainer.ReceivePositionalInputAt(screenSpacePos);

private OsuResumeOverlay.OsuResumeOverlayInputBlocker? resumeInputBlocker;

public void AttachResumeOverlayInputBlocker(OsuResumeOverlay.OsuResumeOverlayInputBlocker resumeInputBlocker)
{
Debug.Assert(this.resumeInputBlocker == null);
this.resumeInputBlocker = resumeInputBlocker;
AddInternal(resumeInputBlocker);
}

private partial class ProxyContainer : LifetimeManagementContainer
{
public void Add(Drawable proxy) => AddInternal(proxy);
Expand Down
50 changes: 45 additions & 5 deletions osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,30 @@ public partial class OsuResumeOverlay : ResumeOverlay
[BackgroundDependencyLoader]
private void load()
{
OsuResumeOverlayInputBlocker? inputBlocker = null;

if (drawableRuleset != null)
{
var osuPlayfield = (OsuPlayfield)drawableRuleset.Playfield;
osuPlayfield.AttachResumeOverlayInputBlocker(inputBlocker = new OsuResumeOverlayInputBlocker());
}

Add(cursorScaleContainer = new Container
{
Child = clickToResumeCursor = new OsuClickToResumeCursor { ResumeRequested = Resume }
Child = clickToResumeCursor = new OsuClickToResumeCursor
{
ResumeRequested = () =>
{
// 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)
inputBlocker.BlockNextPress = true;
Resume();
}
}
});
}

Expand Down Expand Up @@ -115,10 +136,7 @@ public bool OnPressed(KeyBindingPressEvent<OsuAction> e)
return false;

scaleTransitionContainer.ScaleTo(2, TRANSITION_TIME, Easing.OutQuint);

// When resuming with a button, we do not want the osu! input manager to see this button press and include it in the score.
// To ensure that this works correctly, schedule the resume operation one frame forward, since the resume operation enables the input manager to see input events.
Schedule(() => ResumeRequested?.Invoke());
ResumeRequested?.Invoke();
return true;
}

Expand All @@ -143,5 +161,27 @@ private void updateColour()
this.FadeColour(IsHovered ? Color4.White : Color4.Orange, 400, Easing.OutQuint);
}
}

public partial class OsuResumeOverlayInputBlocker : Drawable, IKeyBindingHandler<OsuAction>
{
public bool BlockNextPress;

public OsuResumeOverlayInputBlocker()
{
RelativeSizeAxes = Axes.Both;
Depth = float.MinValue;
}

public bool OnPressed(KeyBindingPressEvent<OsuAction> e)
{
bool block = BlockNextPress;
BlockNextPress = false;
return block;
}

public void OnReleased(KeyBindingReleaseEvent<OsuAction> e)
{
}
}
}
}
102 changes: 75 additions & 27 deletions osu.Game.Tests/Visual/Gameplay/TestScenePauseInputHandling.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Framework.Timing;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mania;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Screens.Play.HUD;
using osu.Game.Skinning;
Expand All @@ -32,6 +34,23 @@ public partial class TestScenePauseInputHandling : PlayerTestScene
[Resolved]
private AudioManager audioManager { get; set; } = null!;

protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new Beatmap
{
HitObjects =
{
new HitCircle
{
Position = OsuPlayfield.BASE_SIZE / 2,
StartTime = 0,
},
new HitCircle
{
Position = OsuPlayfield.BASE_SIZE / 2,
StartTime = 5000,
}
}
};

protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard? storyboard = null) =>
new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audioManager);

Expand Down Expand Up @@ -70,18 +89,16 @@ public void TestOsuInputNotReceivedWhilePaused()
AddStep("resume", () => Player.Resume());
AddStep("go to resume cursor", () => InputManager.MoveMouseTo(this.ChildrenOfType<OsuResumeOverlay.OsuClickToResumeCursor>().Single()));
AddStep("press Z to resume", () => InputManager.PressKey(Key.Z));

// Z key was released before pause, resuming should not trigger it
checkKey(() => counter, 1, false);
checkKey(() => counter, 2, true);

AddStep("release Z", () => InputManager.ReleaseKey(Key.Z));
checkKey(() => counter, 1, false);
checkKey(() => counter, 2, false);

AddStep("press Z", () => InputManager.PressKey(Key.Z));
checkKey(() => counter, 2, true);
checkKey(() => counter, 3, true);

AddStep("release Z", () => InputManager.ReleaseKey(Key.Z));
checkKey(() => counter, 2, false);
checkKey(() => counter, 3, false);
}

[Test]
Expand All @@ -90,30 +107,29 @@ public void TestManiaInputNotReceivedWhilePaused()
KeyCounter counter = null!;

loadPlayer(() => new ManiaRuleset());
AddStep("get key counter", () => counter = this.ChildrenOfType<KeyCounter>().Single(k => k.Trigger is KeyCounterActionTrigger<ManiaAction> actionTrigger && actionTrigger.Action == ManiaAction.Key1));
AddStep("get key counter", () => counter = this.ChildrenOfType<KeyCounter>().Single(k => k.Trigger is KeyCounterActionTrigger<ManiaAction> actionTrigger && actionTrigger.Action == ManiaAction.Special1));
checkKey(() => counter, 0, false);

AddStep("press D", () => InputManager.PressKey(Key.D));
AddStep("press space", () => InputManager.PressKey(Key.Space));
checkKey(() => counter, 1, true);

AddStep("release D", () => InputManager.ReleaseKey(Key.D));
AddStep("release space", () => InputManager.ReleaseKey(Key.Space));
checkKey(() => counter, 1, false);

AddStep("pause", () => Player.Pause());
AddStep("press D", () => InputManager.PressKey(Key.D));
AddStep("press space", () => InputManager.PressKey(Key.Space));
checkKey(() => counter, 1, false);

AddStep("release D", () => InputManager.ReleaseKey(Key.D));
AddStep("release space", () => InputManager.ReleaseKey(Key.Space));
checkKey(() => counter, 1, false);

AddStep("resume", () => Player.Resume());
AddUntilStep("wait for resume", () => Player.GameplayClockContainer.IsRunning);
checkKey(() => counter, 1, false);

AddStep("press D", () => InputManager.PressKey(Key.D));
AddStep("press space", () => InputManager.PressKey(Key.Space));
checkKey(() => counter, 2, true);

AddStep("release D", () => InputManager.ReleaseKey(Key.D));
AddStep("release space", () => InputManager.ReleaseKey(Key.Space));
checkKey(() => counter, 2, false);
}

Expand Down Expand Up @@ -145,8 +161,11 @@ public void TestOsuPreviouslyHeldInputReleaseOnResume()
AddStep("resume", () => Player.Resume());
AddStep("go to resume cursor", () => InputManager.MoveMouseTo(this.ChildrenOfType<OsuResumeOverlay.OsuClickToResumeCursor>().Single()));
AddStep("press Z to resume", () => InputManager.PressKey(Key.Z));
checkKey(() => counterZ, 1, false);
checkKey(() => counterZ, 2, true);
checkKey(() => counterX, 1, false);

AddStep("release Z", () => InputManager.ReleaseKey(Key.Z));
checkKey(() => counterZ, 2, false);
}

[Test]
Expand All @@ -155,12 +174,12 @@ public void TestManiaPreviouslyHeldInputReleaseOnResume()
KeyCounter counter = null!;

loadPlayer(() => new ManiaRuleset());
AddStep("get key counter", () => counter = this.ChildrenOfType<KeyCounter>().Single(k => k.Trigger is KeyCounterActionTrigger<ManiaAction> actionTrigger && actionTrigger.Action == ManiaAction.Key1));
AddStep("get key counter", () => counter = this.ChildrenOfType<KeyCounter>().Single(k => k.Trigger is KeyCounterActionTrigger<ManiaAction> actionTrigger && actionTrigger.Action == ManiaAction.Special1));

AddStep("press D", () => InputManager.PressKey(Key.D));
AddStep("press space", () => InputManager.PressKey(Key.Space));
AddStep("pause", () => Player.Pause());

AddStep("release D", () => InputManager.ReleaseKey(Key.D));
AddStep("release space", () => InputManager.ReleaseKey(Key.Space));
checkKey(() => counter, 1, true);

AddStep("resume", () => Player.Resume());
Expand Down Expand Up @@ -202,12 +221,14 @@ public void TestOsuHeldInputRemainHeldAfterResume()
AddStep("resume", () => Player.Resume());
AddStep("go to resume cursor", () => InputManager.MoveMouseTo(this.ChildrenOfType<OsuResumeOverlay.OsuClickToResumeCursor>().Single()));
AddStep("press Z to resume", () => InputManager.PressKey(Key.Z));
checkKey(() => counterZ, 1, false);
checkKey(() => counterZ, 2, true);
checkKey(() => counterX, 1, true);

AddStep("release X", () => InputManager.ReleaseKey(Key.X));
checkKey(() => counterZ, 1, false);
checkKey(() => counterX, 1, false);

AddStep("release Z", () => InputManager.ReleaseKey(Key.Z));
checkKey(() => counterZ, 2, false);
}

[Test]
Expand All @@ -216,22 +237,48 @@ public void TestManiaHeldInputRemainHeldAfterResume()
KeyCounter counter = null!;

loadPlayer(() => new ManiaRuleset());
AddStep("get key counter", () => counter = this.ChildrenOfType<KeyCounter>().Single(k => k.Trigger is KeyCounterActionTrigger<ManiaAction> actionTrigger && actionTrigger.Action == ManiaAction.Key1));
AddStep("get key counter", () => counter = this.ChildrenOfType<KeyCounter>().Single(k => k.Trigger is KeyCounterActionTrigger<ManiaAction> actionTrigger && actionTrigger.Action == ManiaAction.Special1));

AddStep("press D", () => InputManager.PressKey(Key.D));
AddStep("press space", () => InputManager.PressKey(Key.Space));
checkKey(() => counter, 1, true);

AddStep("pause", () => Player.Pause());

AddStep("release D", () => InputManager.ReleaseKey(Key.D));
AddStep("press D", () => InputManager.PressKey(Key.D));
AddStep("release space", () => InputManager.ReleaseKey(Key.Space));
AddStep("press space", () => InputManager.PressKey(Key.Space));

AddStep("resume", () => Player.Resume());
AddUntilStep("wait for resume", () => Player.GameplayClockContainer.IsRunning);
checkKey(() => counter, 1, true);

AddStep("release D", () => InputManager.ReleaseKey(Key.D));
AddStep("release space", () => InputManager.ReleaseKey(Key.Space));
checkKey(() => counter, 1, false);
}

[Test]
public void TestOsuRegisterInputFromPressingOrangeCursorButPressIsBlocked()
{
KeyCounter counter = null!;

loadPlayer(() => new OsuRuleset());
AddStep("get key counter", () => counter = this.ChildrenOfType<KeyCounter>().Single(k => k.Trigger is KeyCounterActionTrigger<OsuAction> actionTrigger && actionTrigger.Action == OsuAction.LeftButton));

AddStep("pause", () => Player.Pause());
AddStep("resume", () => Player.Resume());
AddStep("go to resume cursor", () => InputManager.MoveMouseTo(this.ChildrenOfType<OsuResumeOverlay.OsuClickToResumeCursor>().Single()));
AddStep("press Z to resume", () => InputManager.PressKey(Key.Z));

// ensure the input manager receives the Z button press...
checkKey(() => counter, 1, true);
AddAssert("button is pressed in kbc", () => Player.DrawableRuleset.Playfield.FindClosestParent<OsuInputManager>()!.PressedActions.Single() == OsuAction.LeftButton);

// ...but also ensure the hit circle in front of the cursor isn't hit by checking max combo.
AddAssert("circle not hit", () => Player.ScoreProcessor.HighestCombo.Value, () => Is.EqualTo(0));

AddStep("release Z", () => InputManager.ReleaseKey(Key.Z));

checkKey(() => counter, 1, false);
AddAssert("button is released in kbc", () => !Player.DrawableRuleset.Playfield.FindClosestParent<OsuInputManager>()!.PressedActions.Any());
}

private void loadPlayer(Func<Ruleset> createRuleset)
Expand All @@ -241,9 +288,10 @@ private void loadPlayer(Func<Ruleset> createRuleset)
AddUntilStep("player loaded", () => Player.IsLoaded && Player.Alpha == 1);
AddUntilStep("wait for hud", () => Player.HUDOverlay.ChildrenOfType<SkinComponentsContainer>().All(s => s.ComponentsLoaded));

AddStep("seek to gameplay", () => Player.GameplayClockContainer.Seek(20000));
AddUntilStep("wait for seek to finish", () => Player.DrawableRuleset.FrameStableClock.CurrentTime, () => Is.EqualTo(20000).Within(500));
AddStep("seek to gameplay", () => Player.GameplayClockContainer.Seek(0));
AddUntilStep("wait for seek to finish", () => Player.DrawableRuleset.FrameStableClock.CurrentTime, () => Is.EqualTo(0).Within(500));
AddAssert("not in break", () => !Player.IsBreakTime.Value);
AddStep("move cursor to center", () => InputManager.MoveMouseTo(Player.DrawableRuleset.Playfield));
}

private void checkKey(Func<KeyCounter> counter, int count, bool active)
Expand Down
2 changes: 1 addition & 1 deletion osu.Game/osu.Game.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Realm" Version="11.5.0" />
<PackageReference Include="ppy.osu.Framework" Version="2024.720.0" />
<PackageReference Include="ppy.osu.Framework" Version="2024.802.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2024.802.0" />
<PackageReference Include="Sentry" Version="4.3.0" />
<!-- Held back due to 0.34.0 failing AOT compilation on ZstdSharp.dll dependency. -->
Expand Down
2 changes: 1 addition & 1 deletion osu.iOS.props
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,6 @@
<RuntimeIdentifier>iossimulator-x64</RuntimeIdentifier>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Framework.iOS" Version="2024.720.0" />
<PackageReference Include="ppy.osu.Framework.iOS" Version="2024.802.0" />
</ItemGroup>
</Project>

0 comments on commit 95aab6c

Please sign in to comment.