From 94d3814ae342290ac1af5da1c95dbc1a405ae0e2 Mon Sep 17 00:00:00 2001 From: David Zhao Date: Mon, 21 Jan 2019 14:40:28 +0900 Subject: [PATCH] Add tests for slider input behavior --- .../TestCaseSliderInput.cs | 406 ++++++++++++++++++ 1 file changed, 406 insertions(+) create mode 100644 osu.Game.Rulesets.Osu.Tests/TestCaseSliderInput.cs diff --git a/osu.Game.Rulesets.Osu.Tests/TestCaseSliderInput.cs b/osu.Game.Rulesets.Osu.Tests/TestCaseSliderInput.cs new file mode 100644 index 0000000000..ebfc11a961 --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/TestCaseSliderInput.cs @@ -0,0 +1,406 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using System.Collections.Generic; +using System.Linq; +using NUnit.Framework; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Replays; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.Objects.Drawables; +using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces; +using osu.Game.Rulesets.Osu.Replays; +using osu.Game.Rulesets.Replays; +using osu.Game.Rulesets.Scoring; +using osu.Game.Scoring; +using osu.Game.Screens.Play; +using osu.Game.Tests.Beatmaps; +using osu.Game.Tests.Visual; +using osuTK; + +namespace osu.Game.Rulesets.Osu.Tests +{ + public class TestCaseSliderInput : OsuTestCase + { + public override IReadOnlyList RequiredTypes => new[] + { + typeof(Slider), + typeof(SliderBall), + typeof(SliderBody), + typeof(SliderTick), + typeof(DrawableSlider), + typeof(DrawableSliderTick), + typeof(DrawableRepeatPoint), + typeof(DrawableOsuHitObject) + }; + + [SetUp] + public void Setup() + { + Schedule(() => { allJudgedFired = false; }); + judgementResults = new List(); + } + + private readonly Container content; + protected override Container Content => content; + + private List judgementResults; + private bool allJudgedFired; + + public TestCaseSliderInput() + { + base.Content.Add(content = new OsuInputManager(new RulesetInfo { ID = 0 })); + } + + /// + /// Pressing a key before a slider, pressing the other key on the slider head, then releasing the latter pressed key + /// should result in tracking to end. + /// At 250ms intervals: + /// Frame 1 (prior to slider): Left Click + /// Frame 2 (within slider hit window): Left & Right Click + /// Frame 3 (while tracking): Left Click + /// A passing test case will have the cursor lose tracking on frame 3. + /// + [Test] + public void TestLeftBeforeSliderThenRight() + { + AddStep("Invalid key transfer test", () => + { + var frames = new List + { + new OsuReplayFrame { Position = new Vector2(0, 0), Actions = { OsuAction.LeftButton }, Time = 250}, + new OsuReplayFrame { Position = new Vector2(0, 0), Actions = { OsuAction.LeftButton, OsuAction.RightButton }, Time = 1500}, + new OsuReplayFrame { Position = new Vector2(0, 0), Actions = { OsuAction.LeftButton }, Time = 2500}, + }; + + performStaticInputTest(frames); + }); + + AddUntilStep(() => allJudgedFired, "Wait for test 1"); + AddAssert("Tracking lost", assertMehJudge); + } + + /// + /// Hitting a slider head, pressing a new key after the initial hit, then letting go of the original key used to hit + /// the slider should reslt in continued tracking. + /// At 250ms intervals: + /// Frame 1: Left Click + /// Frame 2: Left & Right Click + /// Frame 3: Right Click + /// A passing test case will have the cursor continue to track after frame 3. + /// + [Test] + public void TestLeftBeforeSliderThenRightThenLettingGoOfLeft() + { + AddStep("Left to both to right test", () => + { + var frames = new List + { + new OsuReplayFrame { Position = new Vector2(0, 0), Actions = { OsuAction.LeftButton }, Time = 1500}, + new OsuReplayFrame { Position = new Vector2(0, 0), Actions = { OsuAction.LeftButton, OsuAction.RightButton }, Time = 2500}, + new OsuReplayFrame { Position = new Vector2(0, 0), Actions = { OsuAction.RightButton }, Time = 3500}, + }; + + performStaticInputTest(frames); + }); + + AddUntilStep(() => allJudgedFired, "Wait for test 2"); + AddAssert("Tracking retained", assertGreatJudge); + } + + /// + /// Hitting a slider head, pressing a new key after the initial hit, then letting go of the new key should result + /// in continue tracking, + /// At 250ms intervals: + /// Frame 1: Left Click + /// Frame 2: Left & Right Click + /// Frame 3: Left Click + /// A passing test case will have the cursor continue to track after frame 3. + /// + [Test] + public void TestTrackingRetentionLeftRightLeft() + { + AddStep("Tracking retention test", () => + { + var frames = new List + { + new OsuReplayFrame { Position = new Vector2(0, 0), Actions = { OsuAction.LeftButton }, Time = 250}, + new OsuReplayFrame { Position = new Vector2(0, 0), Actions = { OsuAction.LeftButton, OsuAction.RightButton }, Time = 1500}, + new OsuReplayFrame { Position = new Vector2(0, 0), Actions = { OsuAction.RightButton }, Time = 2500}, + }; + + performStaticInputTest(frames); + }); + + AddUntilStep(() => allJudgedFired, "Wait for test 3"); + AddAssert("Tracking retained", assertGreatJudge); + } + + /// + /// Pressing a key before a slider, pressing the other key on the slider head, then releasing the former pressed key + /// should result in continued tracking. + /// At 250ms intervals: + /// Frame 1 (prior to slider): Left Click + /// Frame 2 (on slider head): Left & Right Click + /// Frame 3 (tracking slider body): Right Click + /// A passing test case will have the cursor continue to track after frame 3. + /// + [Test] + public void TestTrackingLeftBeforeSliderToRight() + { + AddStep("Tracking retention test", () => + { + var frames = new List + { + new OsuReplayFrame { Position = new Vector2(0, 0), Actions = { OsuAction.LeftButton }, Time = 250}, + new OsuReplayFrame { Position = new Vector2(0, 0), Actions = { OsuAction.LeftButton, OsuAction.RightButton }, Time = 1500}, + new OsuReplayFrame { Position = new Vector2(0, 0), Actions = { OsuAction.RightButton }, Time = 2500}, + }; + + performStaticInputTest(frames); + }); + + AddUntilStep(() => allJudgedFired, "Wait for test 4"); + AddAssert("Tracking retained", assertGreatJudge); + } + + /// + /// Pressing a key before a slider and holding the slider throughout the body should result in tracking, but a miss on the slider head. + /// Only one frame is required: + /// Frame 1: Left Click + /// In a successful test case: + /// The head of the slider should be judged as a miss. + /// The slider end should be judged as a meh. + /// + [Test] + public void TestTrackingPreclicked() + { + AddStep("Tracking retention test", () => + { + var frames = new List + { + new OsuReplayFrame { Position = new Vector2(0, 0), Actions = { OsuAction.LeftButton }, Time = 250}, + }; + + performStaticInputTest(frames); + }); + + AddUntilStep(() => allJudgedFired, "Wait for test 5"); + AddAssert("Tracking retained, sliderhead miss", assertHeadMissTailMeh); + } + + /// + /// Hitting a slider head, leaving the slider, then coming back into the slider with the same button to track it should re-start tracking. + /// Only one frame is required: + /// Frame 1: Left Click + /// In a successful test case: + /// The last tick of the slider should be judged as a great. + /// + [Test] + public void TestTrackingReturnMidSlider() + { + AddStep("Mid-sldier tracking re-acquisition", () => + { + var frames = new List + { + new OsuReplayFrame { Position = new Vector2(0, 0), Actions = { OsuAction.LeftButton }, Time = 1500}, + new OsuReplayFrame { Position = new Vector2(150, 150), Actions = { OsuAction.LeftButton }, Time = 2000}, + new OsuReplayFrame { Position = new Vector2(200, 200), Actions = { OsuAction.LeftButton }, Time = 2500}, + new OsuReplayFrame { Position = new Vector2(0, 0), Actions = { OsuAction.LeftButton }, Time = 3000}, + new OsuReplayFrame { Position = new Vector2(0, 0), Actions = { OsuAction.LeftButton }, Time = 3500}, + }; + + performStaticInputTest(frames); + }); + + AddUntilStep(() => allJudgedFired, "Wait for test 6"); + AddAssert("Tracking re-acquired", assertMidSliderJudgements); + } + + /// + /// Pressing a key before a slider, hitting a slider head, leaving the slider, then coming back into the slider to track it should NOT start retracking + /// This is current stable behavior. + /// + [Test] + public void TestTrackingReturnMidSliderKeyDownBefore() + { + AddStep("Key held down before slider, mid-slider tracking re-acquisition", () => + { + var frames = new List + { + new OsuReplayFrame { Position = new Vector2(0, 0), Actions = { OsuAction.LeftButton }, Time = 250}, + new OsuReplayFrame { Position = new Vector2(0, 0), Actions = { OsuAction.LeftButton, OsuAction.RightButton }, Time = 1500}, + new OsuReplayFrame { Position = new Vector2(0, 0), Actions = { OsuAction.LeftButton }, Time = 2000}, + new OsuReplayFrame { Position = new Vector2(200, 200), Actions = { OsuAction.LeftButton }, Time = 2500}, + new OsuReplayFrame { Position = new Vector2(0, 0), Actions = { OsuAction.LeftButton }, Time = 3000}, + new OsuReplayFrame { Position = new Vector2(0, 0), Actions = { OsuAction.LeftButton }, Time = 3500}, + }; + + performStaticInputTest(frames); + }); + + AddUntilStep(() => allJudgedFired, "Wait for test 7"); + AddAssert("Tracking lost", assertMidSliderJudgementFail); + } + + /// + /// Halfway into a slider outside of the slider, then starting to hover the slider afterwards should result in tracking + /// + [Test] + public void TestTrackingMidSlider() + { + AddStep("Mid-slider new tracking acquisition", () => + { + var frames = new List + { + new OsuReplayFrame { Position = new Vector2(150, 150), Actions = { OsuAction.LeftButton }, Time = 2000}, + new OsuReplayFrame { Position = new Vector2(200, 200), Actions = { OsuAction.LeftButton }, Time = 2500}, + new OsuReplayFrame { Position = new Vector2(0, 0), Actions = { OsuAction.LeftButton }, Time = 3000}, + new OsuReplayFrame { Position = new Vector2(0, 0), Actions = { OsuAction.LeftButton }, Time = 3500}, + }; + + performStaticInputTest(frames); + }); + + AddUntilStep(() => allJudgedFired, "Wait for test 8"); + AddAssert("Tracking acquired", assertMidSliderJudgements); + } + + /// + /// Pressing a key before a slider, clicking another key after the slider, holding both of them and + /// leaving tracking, then releasing both keys, then pressing the originally pressed key should start tracking + /// + [Test] + public void TestTrackingPressBeforeSliderClickingOtherKeyLeavingSliderReleaseThenTrackOriginal() + { + AddStep("Mid-slider new tracking acquisition", () => + { + var frames = new List + { + new OsuReplayFrame { Position = new Vector2(0, 0), Actions = { OsuAction.LeftButton }, Time = 250}, + new OsuReplayFrame { Position = new Vector2(0, 0), Actions = { OsuAction.LeftButton, OsuAction.RightButton }, Time = 1500}, + new OsuReplayFrame { Position = new Vector2(100, 100), Time = 1750}, + new OsuReplayFrame { Position = new Vector2(0, 0), Actions = { OsuAction.LeftButton }, Time = 3500}, + }; + + performStaticInputTest(frames); + }); + + AddUntilStep(() => allJudgedFired, "Wait for test 9"); + AddAssert("Tracking acquired", assertMidSliderJudgements); + } + + /// + /// Pressing a key before a slider, clicking another key after the slider, holding both of them and + /// leaving tracking, then releasing both keys, then pressing the originally pressed key should start tracking + /// + [Test] + public void TestClickingBeforeLeavingSliderReleasingClickingAgainThenTracking() + { + AddStep("Mid-slider new tracking acquisition", () => + { + var frames = new List + { + new OsuReplayFrame { Position = new Vector2(0, 0), Actions = { OsuAction.LeftButton }, Time = 1500}, + new OsuReplayFrame { Position = new Vector2(100, 100), Actions = { OsuAction.LeftButton }, Time = 2500}, + new OsuReplayFrame { Position = new Vector2(100, 100), Time = 2750}, + new OsuReplayFrame { Position = new Vector2(100, 100), Actions = { OsuAction.LeftButton }, Time = 3000}, + new OsuReplayFrame { Position = new Vector2(0, 0), Actions = { OsuAction.LeftButton }, Time = 3500}, + }; + + performStaticInputTest(frames); + }); + + AddUntilStep(() => allJudgedFired, "Wait for test 10"); + AddAssert("Tracking acquired", assertMidSliderJudgements); + } + + private bool assertMehJudge() + { + return judgementResults.Last().Type == HitResult.Meh; + } + + private bool assertGreatJudge() + { + return judgementResults.Last().Type == HitResult.Great; + } + + private bool assertHeadMissTailMeh() + { + return judgementResults.Last().Type == HitResult.Meh && judgementResults.First().Type == HitResult.Miss; + } + + private bool assertMidSliderJudgements() + { + return judgementResults[judgementResults.Count - 2].Type == HitResult.Great; + } + + private bool assertMidSliderJudgementFail() + { + return judgementResults[judgementResults.Count - 2].Type == HitResult.Miss; + } + + private void performStaticInputTest(List frames) + { + var slider = new Slider + { + StartTime = 1500, + Position = new Vector2(0, 0), + Path = new SliderPath(PathType.PerfectCurve, new[] + { + Vector2.Zero, + new Vector2(25, 0), + }, 25), + }; + + // Empty frame to be added as a workaround for first frame behavior. + // If an input exists on the first frame, the input would apply to the entire intro lead-in + // Likely requires some discussion regarding how first frame inputs should be handled. + frames.Insert(0, new OsuReplayFrame { Position = slider.Position, Time = 0, Actions = new List() }); + + Beatmap.Value = new TestWorkingBeatmap(new Beatmap + { + HitObjects = { slider }, + ControlPointInfo = + { + DifficultyPoints = { new DifficultyControlPoint { SpeedMultiplier = 0.1f } } + }, + BeatmapInfo = + { + BaseDifficulty = new BeatmapDifficulty { SliderTickRate = 3 }, + Ruleset = new OsuRuleset().RulesetInfo + }, + }); + + var player = new ScoreAccessibleReplayPlayer(new Score { Replay = new Replay { Frames = frames } }) + { + AllowPause = false, + AllowLeadIn = false, + AllowResults = false + }; + + Child = player; + + player.ScoreProcessor.NewJudgement += result => judgementResults.Add(result); + + player.ScoreProcessor.AllJudged += () => { allJudgedFired = true; }; + } + + private class ScoreAccessibleReplayPlayer : ReplayPlayer + { + public new ScoreProcessor ScoreProcessor => base.ScoreProcessor; + + public ScoreAccessibleReplayPlayer(Score score) + : base(score) + { + } + } + } +}