From 742698acab9ba2034ebae716bcd8cc1634d5a13e Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 10 Mar 2020 15:30:24 +0900 Subject: [PATCH 01/19] Add notelock implementation --- .../Objects/Drawables/DrawableHitCircle.cs | 2 +- .../Objects/Drawables/DrawableOsuHitObject.cs | 7 +++++ osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs | 28 +++++++++++++++++-- 3 files changed, 34 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs index da1e666aba..3ca2714511 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs @@ -118,7 +118,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables var result = HitObject.HitWindows.ResultFor(timeOffset); - if (result == HitResult.None) + if (result == HitResult.None || CheckHittable?.Invoke(this) == false) { Shake(Math.Abs(timeOffset) - HitObject.HitWindows.WindowFor(HitResult.Miss)); return; diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs index a677cb6a72..82a81040e4 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using osu.Game.Rulesets.Objects.Drawables; using osu.Framework.Graphics; using osu.Game.Rulesets.Judgements; @@ -16,6 +17,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables // Must be set to update IsHovered as it's used in relax mdo to detect osu hit objects. public override bool HandlePositionalInput => true; + /// + /// Whether this can be hit. + /// If not-null, this will not receive a judgement until this function returns true. + /// + public Func CheckHittable; + protected DrawableOsuHitObject(OsuHitObject hitObject) : base(hitObject) { diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs index 6d1ea4bbfc..9eb2786951 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework.Extensions.IEnumerableExtensions; using osuTK; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -64,7 +65,10 @@ namespace osu.Game.Rulesets.Osu.UI base.Add(h); - followPoints.AddFollowPoints((DrawableOsuHitObject)h); + DrawableOsuHitObject osuHitObject = (DrawableOsuHitObject)h; + osuHitObject.CheckHittable = checkHittable; + + followPoints.AddFollowPoints(osuHitObject); } public override bool Remove(DrawableHitObject h) @@ -72,11 +76,31 @@ namespace osu.Game.Rulesets.Osu.UI bool result = base.Remove(h); if (result) - followPoints.RemoveFollowPoints((DrawableOsuHitObject)h); + { + DrawableOsuHitObject osuHitObject = (DrawableOsuHitObject)h; + osuHitObject.CheckHittable = null; + + followPoints.RemoveFollowPoints(osuHitObject); + } return result; } + private bool checkHittable(DrawableOsuHitObject osuHitObject) + { + var lastObject = HitObjectContainer.AliveObjects.GetPrevious(osuHitObject); + + // Ensure the last object is not alive anymore, in which case always allow the hit. + if (lastObject == null) + return true; + + // Ensure that either the last object has received a judgement or the hit time occurs after the last object's start time. + if (lastObject.Judged || Time.Current > lastObject.HitObject.StartTime) + return true; + + return false; + } + private void onNewResult(DrawableHitObject judgedObject, JudgementResult result) { if (!judgedObject.DisplayResult || !DisplayJudgements.Value) From 80a86102b65b5a2421ef75e1899ff609ae463cb8 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 18 Mar 2020 17:00:48 +0900 Subject: [PATCH 02/19] Add test --- .../TestSceneNoteLock.cs | 180 ++++++++++++++++++ 1 file changed, 180 insertions(+) create mode 100644 osu.Game.Rulesets.Osu.Tests/TestSceneNoteLock.cs diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneNoteLock.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneNoteLock.cs new file mode 100644 index 0000000000..a7416671f6 --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneNoteLock.cs @@ -0,0 +1,180 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.Linq; +using NUnit.Framework; +using osu.Framework.Screens; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Replays; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Osu.Objects; +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.Visual; +using osuTK; + +namespace osu.Game.Rulesets.Osu.Tests +{ + public class TestSceneNoteLock : RateAdjustedBeatmapTestScene + { + private const double time_first_circle = 1500; + private const double time_second_circle = 1600; + + private static readonly Vector2 position_first_circle = Vector2.Zero; + private static readonly Vector2 position_second_circle = new Vector2(80); + + /// + /// Tests clicking the second circle before the first hitobject's start time, while the first hitobject HAS NOT been judged. + /// + [Test] + public void TestClickSecondCircleBeforeFirstCircleTime() + { + performTest(new List + { + new OsuReplayFrame { Time = time_first_circle - 100, Position = position_second_circle, Actions = { OsuAction.LeftButton } } + }); + + addJudgementAssert(HitResult.Miss, HitResult.Miss); + } + + /// + /// Tests clicking the second circle at the first hitobject's start time, while the first hitobject HAS NOT been judged. + /// + [Test] + public void TestClickSecondCircleAtFirstCircleTime() + { + performTest(new List + { + new OsuReplayFrame { Time = time_first_circle, Position = position_second_circle, Actions = { OsuAction.LeftButton } } + }); + + addJudgementAssert(HitResult.Miss, HitResult.Miss); + } + + /// + /// Tests clicking the second circle after the first hitobject's start time, while the first hitobject HAS NOT been judged. + /// + [Test] + public void TestClickSecondCircleAfterFirstCircleTime() + { + performTest(new List + { + new OsuReplayFrame { Time = time_first_circle + 100, Position = position_second_circle, Actions = { OsuAction.LeftButton } } + }); + + addJudgementAssert(HitResult.Miss, HitResult.Great); + } + + /// + /// Tests clicking the second circle before the first hitobject's start time, while the first hitobject HAS been judged. + /// + [Test] + public void TestClickSecondCircleBeforeFirstCircleTimeWithFirstCircleJudged() + { + performTest(new List + { + new OsuReplayFrame { Time = time_first_circle - 200, Position = position_first_circle, Actions = { OsuAction.LeftButton } }, + new OsuReplayFrame { Time = time_first_circle - 100, Position = position_second_circle, Actions = { OsuAction.RightButton } } + }); + + addJudgementAssert(HitResult.Great, HitResult.Great); + } + + private void addJudgementAssert(HitResult firstCircle, HitResult secondCircle) + { + AddAssert($"first circle judgement is {firstCircle}", () => judgementResults.Single(r => r.HitObject.StartTime == time_first_circle).Type == firstCircle); + AddAssert($"second circle judgement is {secondCircle}", () => judgementResults.Single(r => r.HitObject.StartTime == time_second_circle).Type == secondCircle); + } + + private ScoreAccessibleReplayPlayer currentPlayer; + private List judgementResults; + private bool allJudgedFired; + + private void performTest(List frames) + { + AddStep("load player", () => + { + Beatmap.Value = CreateWorkingBeatmap(new Beatmap + { + HitObjects = + { + new TestHitCircle + { + StartTime = time_first_circle, + Position = position_first_circle + }, + new TestHitCircle + { + StartTime = time_second_circle, + Position = position_second_circle + } + }, + BeatmapInfo = + { + BaseDifficulty = new BeatmapDifficulty { SliderTickRate = 3 }, + Ruleset = new OsuRuleset().RulesetInfo + }, + }); + + Beatmap.Value.Beatmap.ControlPointInfo.Add(0, new DifficultyControlPoint { SpeedMultiplier = 0.1f }); + + var p = new ScoreAccessibleReplayPlayer(new Score { Replay = new Replay { Frames = frames } }); + + p.OnLoadComplete += _ => + { + p.ScoreProcessor.NewJudgement += result => + { + if (currentPlayer == p) judgementResults.Add(result); + }; + p.ScoreProcessor.AllJudged += () => + { + if (currentPlayer == p) allJudgedFired = true; + }; + }; + + LoadScreen(currentPlayer = p); + allJudgedFired = false; + judgementResults = new List(); + }); + + AddUntilStep("Beatmap at 0", () => Beatmap.Value.Track.CurrentTime == 0); + AddUntilStep("Wait until player is loaded", () => currentPlayer.IsCurrentScreen()); + AddUntilStep("Wait for all judged", () => allJudgedFired); + } + + private class TestHitCircle : HitCircle + { + protected override HitWindows CreateHitWindows() => new TestHitWindows(); + } + + private class TestHitWindows : HitWindows + { + private static readonly DifficultyRange[] ranges = + { + new DifficultyRange(HitResult.Great, 500, 500, 500), + new DifficultyRange(HitResult.Miss, 1000, 1000, 1000), + }; + + public override bool IsHitResultAllowed(HitResult result) => result == HitResult.Great || result == HitResult.Miss; + + protected override DifficultyRange[] GetRanges() => ranges; + } + + private class ScoreAccessibleReplayPlayer : ReplayPlayer + { + public new ScoreProcessor ScoreProcessor => base.ScoreProcessor; + + protected override bool PauseOnFocusLost => false; + + public ScoreAccessibleReplayPlayer(Score score) + : base(score, false, false) + { + } + } + } +} From 1d680b7a0073b783cee638e64c31d90c966f9deb Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 18 Mar 2020 19:13:25 +0900 Subject: [PATCH 03/19] Better english Co-Authored-By: Dean Herbert --- osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs | 2 +- osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs index 82a81040e4..3e66549ca0 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs @@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables /// /// Whether this can be hit. - /// If not-null, this will not receive a judgement until this function returns true. + /// If non-null, judgements will be ignored (resulting in a shake) whilst the function returns false. /// public Func CheckHittable; diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs index 9eb2786951..643253b1af 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs @@ -90,7 +90,7 @@ namespace osu.Game.Rulesets.Osu.UI { var lastObject = HitObjectContainer.AliveObjects.GetPrevious(osuHitObject); - // Ensure the last object is not alive anymore, in which case always allow the hit. + // If there is no previous object alive, allow the hit. if (lastObject == null) return true; From f285b43a74afd66c6c2ec1dcbe63e4f66f007314 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 19 Mar 2020 17:44:32 +0900 Subject: [PATCH 04/19] Allow simultaneous hitobjects --- osu.Game.Rulesets.Osu.Tests/TestSceneNoteLock.cs | 2 +- osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneNoteLock.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneNoteLock.cs index a7416671f6..59d8727ae1 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneNoteLock.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneNoteLock.cs @@ -53,7 +53,7 @@ namespace osu.Game.Rulesets.Osu.Tests new OsuReplayFrame { Time = time_first_circle, Position = position_second_circle, Actions = { OsuAction.LeftButton } } }); - addJudgementAssert(HitResult.Miss, HitResult.Miss); + addJudgementAssert(HitResult.Miss, HitResult.Great); } /// diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs index 643253b1af..bf91504b00 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs @@ -94,8 +94,9 @@ namespace osu.Game.Rulesets.Osu.UI if (lastObject == null) return true; - // Ensure that either the last object has received a judgement or the hit time occurs after the last object's start time. - if (lastObject.Judged || Time.Current > lastObject.HitObject.StartTime) + // Ensure that either the last object has received a judgement or the hit time occurs at or after the last object's start time. + // Simultaneous hitobjects are allowed to be hit at the same time value to account for edge-cases such as Centipede. + if (lastObject.Judged || Time.Current >= lastObject.HitObject.StartTime) return true; return false; From 12a48d2774dd0e4aa19cdd989b34c7022343ff1e Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 19 Mar 2020 19:16:24 +0900 Subject: [PATCH 05/19] Cause all earlier hitobjects to get missed --- .../TestSceneNoteLock.cs | 13 ++++- .../Objects/Drawables/DrawableOsuHitObject.cs | 6 +++ osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs | 52 +++++++++++++++++++ 3 files changed, 70 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneNoteLock.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneNoteLock.cs index 59d8727ae1..e2b8364f3e 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneNoteLock.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneNoteLock.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Linq; using NUnit.Framework; using osu.Framework.Screens; +using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Replays; @@ -24,6 +25,8 @@ namespace osu.Game.Rulesets.Osu.Tests { private const double time_first_circle = 1500; private const double time_second_circle = 1600; + private const double early_miss_window = 1000; // time after -1000 to -500 is considered a miss + private const double late_miss_window = 500; // time after +500 is considered a miss private static readonly Vector2 position_first_circle = Vector2.Zero; private static readonly Vector2 position_second_circle = new Vector2(80); @@ -40,6 +43,7 @@ namespace osu.Game.Rulesets.Osu.Tests }); addJudgementAssert(HitResult.Miss, HitResult.Miss); + addJudgementOffsetAssert(late_miss_window); } /// @@ -54,6 +58,7 @@ namespace osu.Game.Rulesets.Osu.Tests }); addJudgementAssert(HitResult.Miss, HitResult.Great); + addJudgementOffsetAssert(0); } /// @@ -68,6 +73,7 @@ namespace osu.Game.Rulesets.Osu.Tests }); addJudgementAssert(HitResult.Miss, HitResult.Great); + addJudgementOffsetAssert(100); } /// @@ -91,6 +97,11 @@ namespace osu.Game.Rulesets.Osu.Tests AddAssert($"second circle judgement is {secondCircle}", () => judgementResults.Single(r => r.HitObject.StartTime == time_second_circle).Type == secondCircle); } + private void addJudgementOffsetAssert(double offset) + { + AddAssert($"first circle judged at {offset}", () => Precision.AlmostEquals(judgementResults.Single(r => r.HitObject.StartTime == time_first_circle).TimeOffset, offset, 100)); + } + private ScoreAccessibleReplayPlayer currentPlayer; private List judgementResults; private bool allJudgedFired; @@ -157,7 +168,7 @@ namespace osu.Game.Rulesets.Osu.Tests private static readonly DifficultyRange[] ranges = { new DifficultyRange(HitResult.Great, 500, 500, 500), - new DifficultyRange(HitResult.Miss, 1000, 1000, 1000), + new DifficultyRange(HitResult.Miss, early_miss_window, early_miss_window, early_miss_window), }; public override bool IsHitResultAllowed(HitResult result) => result == HitResult.Great || result == HitResult.Miss; diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs index 3e66549ca0..13829dc2f7 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs @@ -7,6 +7,7 @@ using osu.Framework.Graphics; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Graphics.Containers; +using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Osu.Objects.Drawables { @@ -61,6 +62,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables } } + /// + /// Causes this to get missed, disregarding all conditions in implementations of . + /// + public void MissForcefully() => ApplyResult(r => r.Type = HitResult.Miss); + protected override JudgementResult CreateResult(Judgement judgement) => new OsuJudgementResult(HitObject, judgement); } } diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs index bf91504b00..e36d32d01a 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using osu.Framework.Extensions.IEnumerableExtensions; using osuTK; using osu.Framework.Graphics; @@ -104,6 +105,8 @@ namespace osu.Game.Rulesets.Osu.UI private void onNewResult(DrawableHitObject judgedObject, JudgementResult result) { + missAllEarlier(result); + if (!judgedObject.DisplayResult || !DisplayJudgements.Value) return; @@ -117,6 +120,55 @@ namespace osu.Game.Rulesets.Osu.UI judgementLayer.Add(explosion); } + /// + /// Misses all s occurring earlier than the start time of a judged . + /// + /// The of the judged . + private void missAllEarlier(JudgementResult result) + { + // Hitobjects that count as bonus should not cause other hitobjects to get missed. + // E.g. For the sequence slider-head -> circle -> slider-tick, hitting the tick before the circle should not cause the circle to be missed. + // E.g. For the sequence spinner -> circle -> spinner-bonus, hitting the bonus before the circle should not cause the circle to be missed. + if (result.Judgement.IsBonus) + return; + + // The minimum start time required for hitobjects so that they aren't missed. + double minimumTime = result.HitObject.StartTime; + + foreach (var obj in HitObjectContainer.AliveObjects) + { + if (obj.HitObject.StartTime >= minimumTime) + break; + + attemptMiss(obj); + + foreach (var n in obj.NestedHitObjects) + { + if (n.HitObject.StartTime >= minimumTime) + break; + + attemptMiss(n); + } + } + + static void attemptMiss(DrawableHitObject obj) + { + if (!(obj is DrawableOsuHitObject osuObject)) + throw new InvalidOperationException($"{obj.GetType()} is not a {nameof(DrawableOsuHitObject)}."); + + // Hitobjects that have already been judged cannot be missed. + if (osuObject.Judged) + return; + + // Hitobjects that count as bonus should not be missed. + // For the sequence slider-head -> slider-tick -> circle, hitting the circle before the tick should not cause the tick to be missed. + if (osuObject.Result.Judgement.IsBonus) + return; + + osuObject.MissForcefully(); + } + } + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => HitObjectContainer.ReceivePositionalInputAt(screenSpacePos); private class ApproachCircleProxyContainer : LifetimeManagementContainer From b9277165f788361acb43b3c57da49ce605d44155 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 30 Mar 2020 15:11:15 +0900 Subject: [PATCH 06/19] Refactor test to support custom hitobjects --- .../TestSceneNoteLock.cs | 126 +++++++++++++----- 1 file changed, 94 insertions(+), 32 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneNoteLock.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneNoteLock.cs index e2b8364f3e..af82a05c4f 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneNoteLock.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneNoteLock.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Linq; using NUnit.Framework; +using osu.Framework.Extensions.TypeExtensions; using osu.Framework.Screens; using osu.Framework.Utils; using osu.Game.Beatmaps; @@ -23,8 +24,6 @@ namespace osu.Game.Rulesets.Osu.Tests { public class TestSceneNoteLock : RateAdjustedBeatmapTestScene { - private const double time_first_circle = 1500; - private const double time_second_circle = 1600; private const double early_miss_window = 1000; // time after -1000 to -500 is considered a miss private const double late_miss_window = 500; // time after +500 is considered a miss @@ -37,13 +36,31 @@ namespace osu.Game.Rulesets.Osu.Tests [Test] public void TestClickSecondCircleBeforeFirstCircleTime() { - performTest(new List + const double time_first_circle = 1500; + const double time_second_circle = 1600; + + var hitObjects = new List + { + new TestHitCircle + { + StartTime = time_first_circle, + Position = position_first_circle + }, + new TestHitCircle + { + StartTime = time_second_circle, + Position = position_second_circle + } + }; + + performTest(hitObjects, new List { new OsuReplayFrame { Time = time_first_circle - 100, Position = position_second_circle, Actions = { OsuAction.LeftButton } } }); - addJudgementAssert(HitResult.Miss, HitResult.Miss); - addJudgementOffsetAssert(late_miss_window); + addJudgementAssert(hitObjects[0], HitResult.Miss); + addJudgementAssert(hitObjects[1], HitResult.Miss); + addJudgementOffsetAssert(hitObjects[0], late_miss_window); } /// @@ -52,13 +69,31 @@ namespace osu.Game.Rulesets.Osu.Tests [Test] public void TestClickSecondCircleAtFirstCircleTime() { - performTest(new List + const double time_first_circle = 1500; + const double time_second_circle = 1600; + + var hitObjects = new List + { + new TestHitCircle + { + StartTime = time_first_circle, + Position = position_first_circle + }, + new TestHitCircle + { + StartTime = time_second_circle, + Position = position_second_circle + } + }; + + performTest(hitObjects, new List { new OsuReplayFrame { Time = time_first_circle, Position = position_second_circle, Actions = { OsuAction.LeftButton } } }); - addJudgementAssert(HitResult.Miss, HitResult.Great); - addJudgementOffsetAssert(0); + addJudgementAssert(hitObjects[0], HitResult.Miss); + addJudgementAssert(hitObjects[1], HitResult.Great); + addJudgementOffsetAssert(hitObjects[0], 0); } /// @@ -67,13 +102,31 @@ namespace osu.Game.Rulesets.Osu.Tests [Test] public void TestClickSecondCircleAfterFirstCircleTime() { - performTest(new List + const double time_first_circle = 1500; + const double time_second_circle = 1600; + + var hitObjects = new List + { + new TestHitCircle + { + StartTime = time_first_circle, + Position = position_first_circle + }, + new TestHitCircle + { + StartTime = time_second_circle, + Position = position_second_circle + } + }; + + performTest(hitObjects, new List { new OsuReplayFrame { Time = time_first_circle + 100, Position = position_second_circle, Actions = { OsuAction.LeftButton } } }); - addJudgementAssert(HitResult.Miss, HitResult.Great); - addJudgementOffsetAssert(100); + addJudgementAssert(hitObjects[0], HitResult.Miss); + addJudgementAssert(hitObjects[1], HitResult.Great); + addJudgementOffsetAssert(hitObjects[0], 100); } /// @@ -82,49 +135,58 @@ namespace osu.Game.Rulesets.Osu.Tests [Test] public void TestClickSecondCircleBeforeFirstCircleTimeWithFirstCircleJudged() { - performTest(new List + const double time_first_circle = 1500; + const double time_second_circle = 1600; + + var hitObjects = new List + { + new TestHitCircle + { + StartTime = time_first_circle, + Position = position_first_circle + }, + new TestHitCircle + { + StartTime = time_second_circle, + Position = position_second_circle + } + }; + + performTest(hitObjects, new List { new OsuReplayFrame { Time = time_first_circle - 200, Position = position_first_circle, Actions = { OsuAction.LeftButton } }, new OsuReplayFrame { Time = time_first_circle - 100, Position = position_second_circle, Actions = { OsuAction.RightButton } } }); - addJudgementAssert(HitResult.Great, HitResult.Great); + addJudgementAssert(hitObjects[0], HitResult.Great); + addJudgementAssert(hitObjects[1], HitResult.Great); + addJudgementOffsetAssert(hitObjects[0], -200); // time_first_circle - 200 + addJudgementOffsetAssert(hitObjects[0], -200); // time_second_circle - first_circle_time - 100 } - private void addJudgementAssert(HitResult firstCircle, HitResult secondCircle) + private void addJudgementAssert(OsuHitObject hitObject, HitResult result) { - AddAssert($"first circle judgement is {firstCircle}", () => judgementResults.Single(r => r.HitObject.StartTime == time_first_circle).Type == firstCircle); - AddAssert($"second circle judgement is {secondCircle}", () => judgementResults.Single(r => r.HitObject.StartTime == time_second_circle).Type == secondCircle); + AddAssert($"({hitObject.GetType().ReadableName()} @ {hitObject.StartTime}) judgement is {result}", + () => judgementResults.Single(r => r.HitObject == hitObject).Type == result); } - private void addJudgementOffsetAssert(double offset) + private void addJudgementOffsetAssert(OsuHitObject hitObject, double offset) { - AddAssert($"first circle judged at {offset}", () => Precision.AlmostEquals(judgementResults.Single(r => r.HitObject.StartTime == time_first_circle).TimeOffset, offset, 100)); + AddAssert($"({hitObject.GetType().ReadableName()} @ {hitObject.StartTime}) judged at {offset}", + () => Precision.AlmostEquals(judgementResults.Single(r => r.HitObject == hitObject).TimeOffset, offset, 100)); } private ScoreAccessibleReplayPlayer currentPlayer; private List judgementResults; private bool allJudgedFired; - private void performTest(List frames) + private void performTest(List hitObjects, List frames) { AddStep("load player", () => { Beatmap.Value = CreateWorkingBeatmap(new Beatmap { - HitObjects = - { - new TestHitCircle - { - StartTime = time_first_circle, - Position = position_first_circle - }, - new TestHitCircle - { - StartTime = time_second_circle, - Position = position_second_circle - } - }, + HitObjects = hitObjects, BeatmapInfo = { BaseDifficulty = new BeatmapDifficulty { SliderTickRate = 3 }, From 0d202929921e26afb043ea637bb7c9a722b0f7d6 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 30 Mar 2020 16:14:56 +0900 Subject: [PATCH 07/19] Fix ticks/spinners contributing to notelock --- .../Objects/Drawables/DrawableSlider.cs | 2 +- .../Objects/Drawables/DrawableSliderHead.cs | 2 +- osu.Game.Rulesets.Osu/Objects/Slider.cs | 2 +- .../Objects/SliderHeadCircle.cs | 9 ++++ osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs | 53 +++++++++++-------- 5 files changed, 42 insertions(+), 26 deletions(-) create mode 100644 osu.Game.Rulesets.Osu/Objects/SliderHeadCircle.cs diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index 5c7f4a42b3..b017eacf70 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -124,7 +124,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables case SliderTailCircle tail: return new DrawableSliderTail(slider, tail); - case HitCircle head: + case SliderHeadCircle head: return new DrawableSliderHead(slider, head) { OnShake = Shake }; case SliderTick tick: diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs index c5609b01e0..563282e18f 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs @@ -16,7 +16,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables private readonly Slider slider; - public DrawableSliderHead(Slider slider, HitCircle h) + public DrawableSliderHead(Slider slider, SliderHeadCircle h) : base(h) { this.slider = slider; diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs index db1f46d8e2..e5d6c20738 100644 --- a/osu.Game.Rulesets.Osu/Objects/Slider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs @@ -155,7 +155,7 @@ namespace osu.Game.Rulesets.Osu.Objects break; case SliderEventType.Head: - AddNested(HeadCircle = new SliderCircle + AddNested(HeadCircle = new SliderHeadCircle { StartTime = e.Time, Position = Position, diff --git a/osu.Game.Rulesets.Osu/Objects/SliderHeadCircle.cs b/osu.Game.Rulesets.Osu/Objects/SliderHeadCircle.cs new file mode 100644 index 0000000000..f6d46aeef5 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Objects/SliderHeadCircle.cs @@ -0,0 +1,9 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +namespace osu.Game.Rulesets.Osu.Objects +{ + public class SliderHeadCircle : HitCircle + { + } +} diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs index e36d32d01a..97e002edd0 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs @@ -12,6 +12,7 @@ using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects.Drawables.Connections; using osu.Game.Rulesets.UI; using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.UI.Cursor; using osu.Game.Skinning; @@ -126,10 +127,7 @@ namespace osu.Game.Rulesets.Osu.UI /// The of the judged . private void missAllEarlier(JudgementResult result) { - // Hitobjects that count as bonus should not cause other hitobjects to get missed. - // E.g. For the sequence slider-head -> circle -> slider-tick, hitting the tick before the circle should not cause the circle to be missed. - // E.g. For the sequence spinner -> circle -> spinner-bonus, hitting the bonus before the circle should not cause the circle to be missed. - if (result.Judgement.IsBonus) + if (!contributesToNoteLock(result.HitObject)) return; // The minimum start time required for hitobjects so that they aren't missed. @@ -140,35 +138,44 @@ namespace osu.Game.Rulesets.Osu.UI if (obj.HitObject.StartTime >= minimumTime) break; - attemptMiss(obj); + performMiss(obj); foreach (var n in obj.NestedHitObjects) { if (n.HitObject.StartTime >= minimumTime) break; - attemptMiss(n); + performMiss(n); } } - - static void attemptMiss(DrawableHitObject obj) - { - if (!(obj is DrawableOsuHitObject osuObject)) - throw new InvalidOperationException($"{obj.GetType()} is not a {nameof(DrawableOsuHitObject)}."); - - // Hitobjects that have already been judged cannot be missed. - if (osuObject.Judged) - return; - - // Hitobjects that count as bonus should not be missed. - // For the sequence slider-head -> slider-tick -> circle, hitting the circle before the tick should not cause the tick to be missed. - if (osuObject.Result.Judgement.IsBonus) - return; - - osuObject.MissForcefully(); - } } + private void performMiss(DrawableHitObject obj) + { + if (!(obj is DrawableOsuHitObject osuObject)) + throw new InvalidOperationException($"{obj.GetType()} is not a {nameof(DrawableOsuHitObject)}."); + + // Hitobjects that have already been judged cannot be missed. + if (osuObject.Judged) + return; + + // Hitobjects that count as bonus should not be missed. + // For the sequence slider-head -> slider-tick -> circle, hitting the circle before the tick should not cause the tick to be missed. + if (!contributesToNoteLock(obj.HitObject)) + return; + + osuObject.MissForcefully(); + } + + /// + /// Whether a hitobject contributes to notelock. + /// Only hit circles and slider start circles contribute to notelock. + /// + /// The hitobject to test. + /// Whether contributes to notelock. + private bool contributesToNoteLock(HitObject hitObject) + => hitObject is HitCircle && !(hitObject is SliderTailCircle); + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => HitObjectContainer.ReceivePositionalInputAt(screenSpacePos); private class ApproachCircleProxyContainer : LifetimeManagementContainer From e074c3e5e99f69349471cf21e8a72323f390417b Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 30 Mar 2020 16:15:07 +0900 Subject: [PATCH 08/19] Add additional tests --- .../TestSceneNoteLock.cs | 182 ++++++++++++++++-- 1 file changed, 166 insertions(+), 16 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneNoteLock.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneNoteLock.cs index af82a05c4f..a33fb54ff6 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneNoteLock.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneNoteLock.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Collections.Generic; using System.Linq; using NUnit.Framework; @@ -11,6 +12,8 @@ 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.Replays; using osu.Game.Rulesets.Replays; @@ -27,9 +30,6 @@ namespace osu.Game.Rulesets.Osu.Tests private const double early_miss_window = 1000; // time after -1000 to -500 is considered a miss private const double late_miss_window = 500; // time after +500 is considered a miss - private static readonly Vector2 position_first_circle = Vector2.Zero; - private static readonly Vector2 position_second_circle = new Vector2(80); - /// /// Tests clicking the second circle before the first hitobject's start time, while the first hitobject HAS NOT been judged. /// @@ -38,24 +38,26 @@ namespace osu.Game.Rulesets.Osu.Tests { const double time_first_circle = 1500; const double time_second_circle = 1600; + Vector2 positionFirstCircle = Vector2.Zero; + Vector2 positionSecondCircle = new Vector2(80); var hitObjects = new List { new TestHitCircle { StartTime = time_first_circle, - Position = position_first_circle + Position = positionFirstCircle }, new TestHitCircle { StartTime = time_second_circle, - Position = position_second_circle + Position = positionSecondCircle } }; performTest(hitObjects, new List { - new OsuReplayFrame { Time = time_first_circle - 100, Position = position_second_circle, Actions = { OsuAction.LeftButton } } + new OsuReplayFrame { Time = time_first_circle - 100, Position = positionSecondCircle, Actions = { OsuAction.LeftButton } } }); addJudgementAssert(hitObjects[0], HitResult.Miss); @@ -71,24 +73,26 @@ namespace osu.Game.Rulesets.Osu.Tests { const double time_first_circle = 1500; const double time_second_circle = 1600; + Vector2 positionFirstCircle = Vector2.Zero; + Vector2 positionSecondCircle = new Vector2(80); var hitObjects = new List { new TestHitCircle { StartTime = time_first_circle, - Position = position_first_circle + Position = positionFirstCircle }, new TestHitCircle { StartTime = time_second_circle, - Position = position_second_circle + Position = positionSecondCircle } }; performTest(hitObjects, new List { - new OsuReplayFrame { Time = time_first_circle, Position = position_second_circle, Actions = { OsuAction.LeftButton } } + new OsuReplayFrame { Time = time_first_circle, Position = positionSecondCircle, Actions = { OsuAction.LeftButton } } }); addJudgementAssert(hitObjects[0], HitResult.Miss); @@ -104,24 +108,26 @@ namespace osu.Game.Rulesets.Osu.Tests { const double time_first_circle = 1500; const double time_second_circle = 1600; + Vector2 positionFirstCircle = Vector2.Zero; + Vector2 positionSecondCircle = new Vector2(80); var hitObjects = new List { new TestHitCircle { StartTime = time_first_circle, - Position = position_first_circle + Position = positionFirstCircle }, new TestHitCircle { StartTime = time_second_circle, - Position = position_second_circle + Position = positionSecondCircle } }; performTest(hitObjects, new List { - new OsuReplayFrame { Time = time_first_circle + 100, Position = position_second_circle, Actions = { OsuAction.LeftButton } } + new OsuReplayFrame { Time = time_first_circle + 100, Position = positionSecondCircle, Actions = { OsuAction.LeftButton } } }); addJudgementAssert(hitObjects[0], HitResult.Miss); @@ -137,25 +143,27 @@ namespace osu.Game.Rulesets.Osu.Tests { const double time_first_circle = 1500; const double time_second_circle = 1600; + Vector2 positionFirstCircle = Vector2.Zero; + Vector2 positionSecondCircle = new Vector2(80); var hitObjects = new List { new TestHitCircle { StartTime = time_first_circle, - Position = position_first_circle + Position = positionFirstCircle }, new TestHitCircle { StartTime = time_second_circle, - Position = position_second_circle + Position = positionSecondCircle } }; performTest(hitObjects, new List { - new OsuReplayFrame { Time = time_first_circle - 200, Position = position_first_circle, Actions = { OsuAction.LeftButton } }, - new OsuReplayFrame { Time = time_first_circle - 100, Position = position_second_circle, Actions = { OsuAction.RightButton } } + new OsuReplayFrame { Time = time_first_circle - 200, Position = positionFirstCircle, Actions = { OsuAction.LeftButton } }, + new OsuReplayFrame { Time = time_first_circle - 100, Position = positionSecondCircle, Actions = { OsuAction.RightButton } } }); addJudgementAssert(hitObjects[0], HitResult.Great); @@ -164,12 +172,133 @@ namespace osu.Game.Rulesets.Osu.Tests addJudgementOffsetAssert(hitObjects[0], -200); // time_second_circle - first_circle_time - 100 } + [Test] + public void TestMissSliderHeadAndHitAllSliderTicks() + { + const double time_slider = 1500; + const double time_circle = 1510; + Vector2 positionCircle = Vector2.Zero; + Vector2 positionSlider = new Vector2(80); + + var hitObjects = new List + { + new TestHitCircle + { + StartTime = time_circle, + Position = positionCircle + }, + new TestSlider + { + StartTime = time_slider, + Position = positionSlider, + Path = new SliderPath(PathType.Linear, new[] + { + Vector2.Zero, + new Vector2(25, 0), + }) + } + }; + + performTest(hitObjects, new List + { + new OsuReplayFrame { Time = time_slider, Position = positionCircle, Actions = { OsuAction.LeftButton } }, + new OsuReplayFrame { Time = time_slider + 10, Position = positionSlider, Actions = { OsuAction.RightButton } } + }); + + addJudgementAssert(hitObjects[0], HitResult.Great); + addJudgementAssert(hitObjects[1], HitResult.Great); + addJudgementAssert("slider head", () => ((Slider)hitObjects[1]).HeadCircle, HitResult.Miss); + addJudgementAssert("slider tick", () => ((Slider)hitObjects[1]).NestedHitObjects[1] as SliderTick, HitResult.Great); + } + + [Test] + public void TestHitSliderTicksBeforeCircle() + { + const double time_slider = 1500; + const double time_circle = 1510; + Vector2 positionCircle = Vector2.Zero; + Vector2 positionSlider = new Vector2(80); + + var hitObjects = new List + { + new TestHitCircle + { + StartTime = time_circle, + Position = positionCircle + }, + new TestSlider + { + StartTime = time_slider, + Position = positionSlider, + Path = new SliderPath(PathType.Linear, new[] + { + Vector2.Zero, + new Vector2(25, 0), + }) + } + }; + + performTest(hitObjects, new List + { + new OsuReplayFrame { Time = time_slider, Position = positionSlider, Actions = { OsuAction.LeftButton } }, + new OsuReplayFrame { Time = time_circle + late_miss_window - 100, Position = positionCircle, Actions = { OsuAction.RightButton } }, + new OsuReplayFrame { Time = time_circle + late_miss_window - 90, Position = positionSlider, Actions = { OsuAction.LeftButton } }, + }); + + addJudgementAssert(hitObjects[0], HitResult.Great); + addJudgementAssert(hitObjects[1], HitResult.Great); + addJudgementAssert("slider head", () => ((Slider)hitObjects[1]).HeadCircle, HitResult.Great); + addJudgementAssert("slider tick", () => ((Slider)hitObjects[1]).NestedHitObjects[1] as SliderTick, HitResult.Great); + } + + [Test] + public void TestHitCircleBeforeSpinner() + { + const double time_spinner = 1500; + const double time_circle = 1510; + Vector2 positionCircle = Vector2.Zero; + + var hitObjects = new List + { + new TestSpinner + { + StartTime = time_spinner, + Position = new Vector2(256, 192), + EndTime = time_spinner + 1000, + }, + new TestHitCircle + { + StartTime = time_circle, + Position = positionCircle + }, + }; + + performTest(hitObjects, new List + { + new OsuReplayFrame { Time = time_spinner, Position = positionCircle, Actions = { OsuAction.LeftButton } }, + new OsuReplayFrame { Time = time_spinner + 10, Position = new Vector2(236, 192), Actions = { OsuAction.RightButton } }, + new OsuReplayFrame { Time = time_spinner + 20, Position = new Vector2(256, 172), Actions = { OsuAction.RightButton } }, + new OsuReplayFrame { Time = time_spinner + 30, Position = new Vector2(276, 192), Actions = { OsuAction.RightButton } }, + new OsuReplayFrame { Time = time_spinner + 40, Position = new Vector2(256, 212), Actions = { OsuAction.RightButton } }, + new OsuReplayFrame { Time = time_spinner + 50, Position = new Vector2(236, 192), Actions = { OsuAction.RightButton } }, + }); + + addJudgementAssert(hitObjects[0], HitResult.Great); + addJudgementAssert(hitObjects[1], HitResult.Great); + } + private void addJudgementAssert(OsuHitObject hitObject, HitResult result) { AddAssert($"({hitObject.GetType().ReadableName()} @ {hitObject.StartTime}) judgement is {result}", () => judgementResults.Single(r => r.HitObject == hitObject).Type == result); } + private void addJudgementAssert(string name, Func hitObject, HitResult result) + { + AddAssert($"{name} judgement is {result}", + () => judgementResults.Single(r => r.HitObject == hitObject()).Type == result); + } + private void addJudgementOffsetAssert(OsuHitObject hitObject, double offset) { AddAssert($"({hitObject.GetType().ReadableName()} @ {hitObject.StartTime}) judged at {offset}", @@ -225,6 +354,27 @@ namespace osu.Game.Rulesets.Osu.Tests protected override HitWindows CreateHitWindows() => new TestHitWindows(); } + private class TestSlider : Slider + { + public TestSlider() + { + DefaultsApplied += () => + { + HeadCircle.HitWindows = new TestHitWindows(); + TailCircle.HitWindows = new TestHitWindows(); + }; + } + } + + private class TestSpinner : Spinner + { + protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty) + { + base.ApplyDefaultsToSelf(controlPointInfo, difficulty); + SpinsRequired = 1; + } + } + private class TestHitWindows : HitWindows { private static readonly DifficultyRange[] ranges = From 744f6c3ca7be99511c5732c0fa4c8688a3acbd5e Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 30 Mar 2020 16:33:46 +0900 Subject: [PATCH 09/19] Rename method + adjust comments --- osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs index 97e002edd0..994b3d9718 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs @@ -127,7 +127,7 @@ namespace osu.Game.Rulesets.Osu.UI /// The of the judged . private void missAllEarlier(JudgementResult result) { - if (!contributesToNoteLock(result.HitObject)) + if (!causesNoteLockMisses(result.HitObject)) return; // The minimum start time required for hitobjects so that they aren't missed. @@ -159,21 +159,18 @@ namespace osu.Game.Rulesets.Osu.UI if (osuObject.Judged) return; - // Hitobjects that count as bonus should not be missed. - // For the sequence slider-head -> slider-tick -> circle, hitting the circle before the tick should not cause the tick to be missed. - if (!contributesToNoteLock(obj.HitObject)) + if (!causesNoteLockMisses(obj.HitObject)) return; osuObject.MissForcefully(); } /// - /// Whether a hitobject contributes to notelock. - /// Only hit circles and slider start circles contribute to notelock. + /// Whether a can be missed and causes other hitobjects to be missed during notelock. /// - /// The hitobject to test. - /// Whether contributes to notelock. - private bool contributesToNoteLock(HitObject hitObject) + /// The to test. + /// Whether contributes to notelock misses. + private bool causesNoteLockMisses(HitObject hitObject) => hitObject is HitCircle && !(hitObject is SliderTailCircle); public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => HitObjectContainer.ReceivePositionalInputAt(screenSpacePos); From 796976db3c046f967e0403c9d15e4d305cbe3435 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 30 Mar 2020 17:00:53 +0900 Subject: [PATCH 10/19] Completely ignore spinners from note lock --- osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs index 994b3d9718..db8a47e4a2 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs @@ -90,7 +90,14 @@ namespace osu.Game.Rulesets.Osu.UI private bool checkHittable(DrawableOsuHitObject osuHitObject) { - var lastObject = HitObjectContainer.AliveObjects.GetPrevious(osuHitObject); + DrawableHitObject lastObject = osuHitObject; + + // Get the last hitobject that contributes to note lock + while ((lastObject = HitObjectContainer.AliveObjects.GetPrevious(lastObject)) != null) + { + if (contributesToNoteLock(lastObject.HitObject)) + break; + } // If there is no previous object alive, allow the hit. if (lastObject == null) @@ -166,10 +173,19 @@ namespace osu.Game.Rulesets.Osu.UI } /// - /// Whether a can be missed and causes other hitobjects to be missed during notelock. + /// Whether a is contributes to note lock. + /// Future contributing s will not be hittable until the start time of the last contributing is reached. /// /// The to test. - /// Whether contributes to notelock misses. + /// Whether causes note lock. + private bool contributesToNoteLock(HitObject hitObject) + => hitObject is HitCircle || hitObject is Slider; + + /// + /// Whether a can be missed and causes other s to be missed when hit out-of-order during note lock. + /// + /// The to test. + /// Whether contributes to note lock misses. private bool causesNoteLockMisses(HitObject hitObject) => hitObject is HitCircle && !(hitObject is SliderTailCircle); From 1ff60b73d70deb25d9133f42b17b813711759bbc Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 30 Mar 2020 17:01:29 +0900 Subject: [PATCH 11/19] Refactor tests a bit --- .../TestSceneNoteLock.cs | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneNoteLock.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneNoteLock.cs index a33fb54ff6..2c69540951 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneNoteLock.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneNoteLock.cs @@ -31,7 +31,7 @@ namespace osu.Game.Rulesets.Osu.Tests private const double late_miss_window = 500; // time after +500 is considered a miss /// - /// Tests clicking the second circle before the first hitobject's start time, while the first hitobject HAS NOT been judged. + /// Tests clicking a future circle before the first circle's start time, while the first circle HAS NOT been judged. /// [Test] public void TestClickSecondCircleBeforeFirstCircleTime() @@ -66,7 +66,7 @@ namespace osu.Game.Rulesets.Osu.Tests } /// - /// Tests clicking the second circle at the first hitobject's start time, while the first hitobject HAS NOT been judged. + /// Tests clicking a future circle at the first circle's start time, while the first circle HAS NOT been judged. /// [Test] public void TestClickSecondCircleAtFirstCircleTime() @@ -101,7 +101,7 @@ namespace osu.Game.Rulesets.Osu.Tests } /// - /// Tests clicking the second circle after the first hitobject's start time, while the first hitobject HAS NOT been judged. + /// Tests clicking a future circle after the first circle's start time, while the first circle HAS NOT been judged. /// [Test] public void TestClickSecondCircleAfterFirstCircleTime() @@ -136,7 +136,7 @@ namespace osu.Game.Rulesets.Osu.Tests } /// - /// Tests clicking the second circle before the first hitobject's start time, while the first hitobject HAS been judged. + /// Tests clicking a future circle before the first circle's start time, while the first circle HAS been judged. /// [Test] public void TestClickSecondCircleBeforeFirstCircleTimeWithFirstCircleJudged() @@ -172,6 +172,9 @@ namespace osu.Game.Rulesets.Osu.Tests addJudgementOffsetAssert(hitObjects[0], -200); // time_second_circle - first_circle_time - 100 } + /// + /// Tests clicking a future circle after a slider's start time, but hitting all slider ticks. + /// [Test] public void TestMissSliderHeadAndHitAllSliderTicks() { @@ -211,6 +214,9 @@ namespace osu.Game.Rulesets.Osu.Tests addJudgementAssert("slider tick", () => ((Slider)hitObjects[1]).NestedHitObjects[1] as SliderTick, HitResult.Great); } + /// + /// Tests clicking hitting future slider ticks before a circle. + /// [Test] public void TestHitSliderTicksBeforeCircle() { @@ -251,11 +257,14 @@ namespace osu.Game.Rulesets.Osu.Tests addJudgementAssert("slider tick", () => ((Slider)hitObjects[1]).NestedHitObjects[1] as SliderTick, HitResult.Great); } + /// + /// Tests clicking a future circle before a spinner. + /// [Test] public void TestHitCircleBeforeSpinner() { const double time_spinner = 1500; - const double time_circle = 1510; + const double time_circle = 1800; Vector2 positionCircle = Vector2.Zero; var hitObjects = new List @@ -275,7 +284,7 @@ namespace osu.Game.Rulesets.Osu.Tests performTest(hitObjects, new List { - new OsuReplayFrame { Time = time_spinner, Position = positionCircle, Actions = { OsuAction.LeftButton } }, + new OsuReplayFrame { Time = time_spinner - 100, Position = positionCircle, Actions = { OsuAction.LeftButton } }, new OsuReplayFrame { Time = time_spinner + 10, Position = new Vector2(236, 192), Actions = { OsuAction.RightButton } }, new OsuReplayFrame { Time = time_spinner + 20, Position = new Vector2(256, 172), Actions = { OsuAction.RightButton } }, new OsuReplayFrame { Time = time_spinner + 30, Position = new Vector2(276, 192), Actions = { OsuAction.RightButton } }, From 116b952dfe973218621de51532c8620c0f65e015 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 10 Apr 2020 01:20:43 +0900 Subject: [PATCH 12/19] Change param to hitobject rather than result --- osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs index db8a47e4a2..9c066c367b 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs @@ -113,7 +113,7 @@ namespace osu.Game.Rulesets.Osu.UI private void onNewResult(DrawableHitObject judgedObject, JudgementResult result) { - missAllEarlier(result); + missAllEarlier(result.HitObject); if (!judgedObject.DisplayResult || !DisplayJudgements.Value) return; @@ -131,14 +131,14 @@ namespace osu.Game.Rulesets.Osu.UI /// /// Misses all s occurring earlier than the start time of a judged . /// - /// The of the judged . - private void missAllEarlier(JudgementResult result) + /// The marker , which all s earlier than will get missed. + private void missAllEarlier(HitObject hitObject) { - if (!causesNoteLockMisses(result.HitObject)) + if (!causesNoteLockMisses(hitObject)) return; // The minimum start time required for hitobjects so that they aren't missed. - double minimumTime = result.HitObject.StartTime; + double minimumTime = hitObject.StartTime; foreach (var obj in HitObjectContainer.AliveObjects) { From b8d7b78b55a3022e8556110a44bc4d40c977c86e Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 10 Apr 2020 01:21:37 +0900 Subject: [PATCH 13/19] Remove unnecessary null set --- osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs index 9c066c367b..9011f21fd5 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs @@ -78,12 +78,7 @@ namespace osu.Game.Rulesets.Osu.UI bool result = base.Remove(h); if (result) - { - DrawableOsuHitObject osuHitObject = (DrawableOsuHitObject)h; - osuHitObject.CheckHittable = null; - - followPoints.RemoveFollowPoints(osuHitObject); - } + followPoints.RemoveFollowPoints((DrawableOsuHitObject)h); return result; } From ea1bec85ae7ef875f133add73c5051a6fa9b5a4c Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 10 Apr 2020 01:40:20 +0900 Subject: [PATCH 14/19] Simplify code/language --- osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs | 65 +++++++++--------------- 1 file changed, 24 insertions(+), 41 deletions(-) diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs index 9011f21fd5..f4009a281c 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using osu.Framework.Extensions.IEnumerableExtensions; using osuTK; using osu.Framework.Graphics; @@ -87,10 +86,10 @@ namespace osu.Game.Rulesets.Osu.UI { DrawableHitObject lastObject = osuHitObject; - // Get the last hitobject that contributes to note lock + // Get the last hitobject that can block future hits while ((lastObject = HitObjectContainer.AliveObjects.GetPrevious(lastObject)) != null) { - if (contributesToNoteLock(lastObject.HitObject)) + if (canBlockFutureHits(lastObject.HitObject)) break; } @@ -108,7 +107,9 @@ namespace osu.Game.Rulesets.Osu.UI private void onNewResult(DrawableHitObject judgedObject, JudgementResult result) { - missAllEarlier(result.HitObject); + // Hitobjects that block future hits should miss previous hitobjects if they're hit out-of-order. + if (canBlockFutureHits(result.HitObject)) + missAllEarlierObjects(result.HitObject); if (!judgedObject.DisplayResult || !DisplayJudgements.Value) return; @@ -127,12 +128,8 @@ namespace osu.Game.Rulesets.Osu.UI /// Misses all s occurring earlier than the start time of a judged . /// /// The marker , which all s earlier than will get missed. - private void missAllEarlier(HitObject hitObject) + private void missAllEarlierObjects(HitObject hitObject) { - if (!causesNoteLockMisses(hitObject)) - return; - - // The minimum start time required for hitobjects so that they aren't missed. double minimumTime = hitObject.StartTime; foreach (var obj in HitObjectContainer.AliveObjects) @@ -140,50 +137,36 @@ namespace osu.Game.Rulesets.Osu.UI if (obj.HitObject.StartTime >= minimumTime) break; - performMiss(obj); - - foreach (var n in obj.NestedHitObjects) + switch (obj) { - if (n.HitObject.StartTime >= minimumTime) + case DrawableHitCircle circle: + miss(circle); break; - performMiss(n); + case DrawableSlider slider: + miss(slider.HeadCircle); + break; } } + + static void miss(DrawableOsuHitObject obj) + { + // Hitobjects that have already been judged cannot be missed. + if (obj.Judged) + return; + + obj.MissForcefully(); + } } - private void performMiss(DrawableHitObject obj) - { - if (!(obj is DrawableOsuHitObject osuObject)) - throw new InvalidOperationException($"{obj.GetType()} is not a {nameof(DrawableOsuHitObject)}."); - - // Hitobjects that have already been judged cannot be missed. - if (osuObject.Judged) - return; - - if (!causesNoteLockMisses(obj.HitObject)) - return; - - osuObject.MissForcefully(); - } - /// - /// Whether a is contributes to note lock. - /// Future contributing s will not be hittable until the start time of the last contributing is reached. + /// Whether a can block hits on future s until its start time is reached. /// /// The to test. - /// Whether causes note lock. - private bool contributesToNoteLock(HitObject hitObject) + /// Whether can block hits on future s. + private bool canBlockFutureHits(HitObject hitObject) => hitObject is HitCircle || hitObject is Slider; - /// - /// Whether a can be missed and causes other s to be missed when hit out-of-order during note lock. - /// - /// The to test. - /// Whether contributes to note lock misses. - private bool causesNoteLockMisses(HitObject hitObject) - => hitObject is HitCircle && !(hitObject is SliderTailCircle); - public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => HitObjectContainer.ReceivePositionalInputAt(screenSpacePos); private class ApproachCircleProxyContainer : LifetimeManagementContainer From 10e849d19616d3fa1314e8fa81ea10e12111e1da Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 10 Apr 2020 02:02:09 +0900 Subject: [PATCH 15/19] Separate into separate class --- .../Objects/Drawables/DrawableHitCircle.cs | 2 +- .../Objects/Drawables/DrawableOsuHitObject.cs | 2 +- osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs | 104 ++++++++++++++++++ osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs | 76 +------------ 4 files changed, 111 insertions(+), 73 deletions(-) create mode 100644 osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs index 5776c64c86..d73ad888f4 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs @@ -120,7 +120,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables var result = HitObject.HitWindows.ResultFor(timeOffset); - if (result == HitResult.None || CheckHittable?.Invoke(this) == false) + if (result == HitResult.None || CheckHittable?.Invoke(this, Time.Current) == false) { Shake(Math.Abs(timeOffset) - HitObject.HitWindows.WindowFor(HitResult.Miss)); return; diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs index 13829dc2f7..fe23e3729d 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs @@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables /// Whether this can be hit. /// If non-null, judgements will be ignored (resulting in a shake) whilst the function returns false. /// - public Func CheckHittable; + public Func CheckHittable; protected DrawableOsuHitObject(OsuHitObject hitObject) : base(hitObject) diff --git a/osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs b/osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs new file mode 100644 index 0000000000..ddaf714e5b --- /dev/null +++ b/osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs @@ -0,0 +1,104 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Extensions.IEnumerableExtensions; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.Objects.Drawables; +using osu.Game.Rulesets.UI; + +namespace osu.Game.Rulesets.Osu.UI +{ + /// + /// Ensures that s are hit in-order. + /// If a is hit out of order: + /// + /// The hit is blocked if it occurred earlier than the previous 's start time. + /// The hit causes all previous s to missed otherwise. + /// + /// + public class OrderedHitPolicy + { + private readonly HitObjectContainer hitObjectContainer; + + public OrderedHitPolicy(HitObjectContainer hitObjectContainer) + { + this.hitObjectContainer = hitObjectContainer; + } + + /// + /// Determines whether a can be hit at a point in time. + /// + /// The to check. + /// The time to check. + /// Whether can be hit at the given . + public bool IsHittable(DrawableHitObject hitObject, double time) + { + DrawableHitObject lastObject = hitObject; + + // Get the last hitobject that can block future hits + while ((lastObject = hitObjectContainer.AliveObjects.GetPrevious(lastObject)) != null) + { + if (canBlockFutureHits(lastObject.HitObject)) + break; + } + + // If there is no previous object alive, allow the hit. + if (lastObject == null) + return true; + + // Ensure that either the last object has received a judgement or the hit time occurs at or after the last object's start time. + // Simultaneous hitobjects are allowed to be hit at the same time value to account for edge-cases such as Centipede. + if (lastObject.Judged || time >= lastObject.HitObject.StartTime) + return true; + + return false; + } + + /// + /// Handles a being hit to potentially miss all earlier s. + /// + /// The that was hit. + public void HandleHit(HitObject hitObject) + { + if (!canBlockFutureHits(hitObject)) + return; + + double minimumTime = hitObject.StartTime; + + foreach (var obj in hitObjectContainer.AliveObjects) + { + if (obj.HitObject.StartTime >= minimumTime) + break; + + switch (obj) + { + case DrawableHitCircle circle: + miss(circle); + break; + + case DrawableSlider slider: + miss(slider.HeadCircle); + break; + } + } + + static void miss(DrawableOsuHitObject obj) + { + // Hitobjects that have already been judged cannot be missed. + if (obj.Judged) + return; + + obj.MissForcefully(); + } + } + + /// + /// Whether a blocks hits on future s until its start time is reached. + /// + /// The to test. + private bool canBlockFutureHits(HitObject hitObject) + => hitObject is HitCircle || hitObject is Slider; + } +} diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs index f4009a281c..2f222f59b4 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using osu.Framework.Extensions.IEnumerableExtensions; using osuTK; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -11,7 +10,6 @@ using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects.Drawables.Connections; using osu.Game.Rulesets.UI; using osu.Game.Rulesets.Judgements; -using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.UI.Cursor; using osu.Game.Skinning; @@ -22,6 +20,7 @@ namespace osu.Game.Rulesets.Osu.UI private readonly ApproachCircleProxyContainer approachCircles; private readonly JudgementContainer judgementLayer; private readonly FollowPointRenderer followPoints; + private readonly OrderedHitPolicy hitPolicy; public static readonly Vector2 BASE_SIZE = new Vector2(512, 384); @@ -53,6 +52,8 @@ namespace osu.Game.Rulesets.Osu.UI Depth = -1, }, }; + + hitPolicy = new OrderedHitPolicy(HitObjectContainer); } public override void Add(DrawableHitObject h) @@ -67,7 +68,7 @@ namespace osu.Game.Rulesets.Osu.UI base.Add(h); DrawableOsuHitObject osuHitObject = (DrawableOsuHitObject)h; - osuHitObject.CheckHittable = checkHittable; + osuHitObject.CheckHittable = hitPolicy.IsHittable; followPoints.AddFollowPoints(osuHitObject); } @@ -82,34 +83,10 @@ namespace osu.Game.Rulesets.Osu.UI return result; } - private bool checkHittable(DrawableOsuHitObject osuHitObject) - { - DrawableHitObject lastObject = osuHitObject; - - // Get the last hitobject that can block future hits - while ((lastObject = HitObjectContainer.AliveObjects.GetPrevious(lastObject)) != null) - { - if (canBlockFutureHits(lastObject.HitObject)) - break; - } - - // If there is no previous object alive, allow the hit. - if (lastObject == null) - return true; - - // Ensure that either the last object has received a judgement or the hit time occurs at or after the last object's start time. - // Simultaneous hitobjects are allowed to be hit at the same time value to account for edge-cases such as Centipede. - if (lastObject.Judged || Time.Current >= lastObject.HitObject.StartTime) - return true; - - return false; - } - private void onNewResult(DrawableHitObject judgedObject, JudgementResult result) { // Hitobjects that block future hits should miss previous hitobjects if they're hit out-of-order. - if (canBlockFutureHits(result.HitObject)) - missAllEarlierObjects(result.HitObject); + hitPolicy.HandleHit(result.HitObject); if (!judgedObject.DisplayResult || !DisplayJudgements.Value) return; @@ -124,49 +101,6 @@ namespace osu.Game.Rulesets.Osu.UI judgementLayer.Add(explosion); } - /// - /// Misses all s occurring earlier than the start time of a judged . - /// - /// The marker , which all s earlier than will get missed. - private void missAllEarlierObjects(HitObject hitObject) - { - double minimumTime = hitObject.StartTime; - - foreach (var obj in HitObjectContainer.AliveObjects) - { - if (obj.HitObject.StartTime >= minimumTime) - break; - - switch (obj) - { - case DrawableHitCircle circle: - miss(circle); - break; - - case DrawableSlider slider: - miss(slider.HeadCircle); - break; - } - } - - static void miss(DrawableOsuHitObject obj) - { - // Hitobjects that have already been judged cannot be missed. - if (obj.Judged) - return; - - obj.MissForcefully(); - } - } - - /// - /// Whether a can block hits on future s until its start time is reached. - /// - /// The to test. - /// Whether can block hits on future s. - private bool canBlockFutureHits(HitObject hitObject) - => hitObject is HitCircle || hitObject is Slider; - public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => HitObjectContainer.ReceivePositionalInputAt(screenSpacePos); private class ApproachCircleProxyContainer : LifetimeManagementContainer From b54bbc5f6a217352a03ed77eb05eb20e50a948fa Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 10 Apr 2020 02:41:37 +0900 Subject: [PATCH 16/19] Improve commenting + refactor --- osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs | 79 +++++++++++++------- 1 file changed, 54 insertions(+), 25 deletions(-) diff --git a/osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs b/osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs index ddaf714e5b..0a09b5be7c 100644 --- a/osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs +++ b/osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using osu.Framework.Extensions.IEnumerableExtensions; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects; @@ -35,22 +34,27 @@ namespace osu.Game.Rulesets.Osu.UI /// Whether can be hit at the given . public bool IsHittable(DrawableHitObject hitObject, double time) { - DrawableHitObject lastObject = hitObject; + DrawableHitObject blockingObject = null; - // Get the last hitobject that can block future hits - while ((lastObject = hitObjectContainer.AliveObjects.GetPrevious(lastObject)) != null) + // Find the last hitobject which blocks future hits. + foreach (var obj in hitObjectContainer.AliveObjects) { - if (canBlockFutureHits(lastObject.HitObject)) + if (obj == hitObject) break; + + if (canBlockFutureHits(obj)) + blockingObject = obj; } - // If there is no previous object alive, allow the hit. - if (lastObject == null) + // If there is no previous hitobject, allow the hit. + if (blockingObject == null) return true; - // Ensure that either the last object has received a judgement or the hit time occurs at or after the last object's start time. - // Simultaneous hitobjects are allowed to be hit at the same time value to account for edge-cases such as Centipede. - if (lastObject.Judged || time >= lastObject.HitObject.StartTime) + // A hit is allowed if: + // 1. The last blocking hitobject has been judged. + // 2. The current time is after the last hitobject's start time. + // Hits at exactly the same time as the blocking hitobject are allowed for maps that contain simultaneous hitobjects (e.g. /b/372245). + if (blockingObject.Judged || time >= blockingObject.HitObject.StartTime) return true; return false; @@ -62,6 +66,7 @@ namespace osu.Game.Rulesets.Osu.UI /// The that was hit. public void HandleHit(HitObject hitObject) { + // Hitobjects which themselves don't block future hitobjects don't cause misses (e.g. slider ticks) if (!canBlockFutureHits(hitObject)) return; @@ -72,33 +77,57 @@ namespace osu.Game.Rulesets.Osu.UI if (obj.HitObject.StartTime >= minimumTime) break; - switch (obj) + // If the parent hitobject cannot cause a miss, neither can any nested hitobject. + if (!canBlockFutureHits(obj)) + continue; + + applyMiss(obj); + + foreach (var nested in obj.NestedHitObjects) { - case DrawableHitCircle circle: - miss(circle); + if (nested.HitObject.StartTime >= minimumTime) break; - case DrawableSlider slider: - miss(slider.HeadCircle); - break; + if (canBlockFutureHits(nested)) + applyMiss(nested); } } - static void miss(DrawableOsuHitObject obj) - { - // Hitobjects that have already been judged cannot be missed. - if (obj.Judged) - return; + static void applyMiss(DrawableHitObject obj) => ((DrawableOsuHitObject)obj).MissForcefully(); + } - obj.MissForcefully(); - } + /// + /// Whether a blocks hits on future s until its start time is reached. + /// + /// + /// Must only be used when iterating through top-most drawable hitobjects. + /// + /// The to test. + private static bool canBlockFutureHits(DrawableHitObject hitObject) + { + // Judged hitobjects can never block hits. + if (hitObject.Judged) + return false; + + // Special considerations for slider tails aren't required since only top-most drawable hitobjects are being iterated over. + return hitObject is DrawableHitCircle || hitObject is DrawableSlider; } /// /// Whether a blocks hits on future s until its start time is reached. /// + /// + /// Must only be used when iterating through nested hitobjects. + /// /// The to test. - private bool canBlockFutureHits(HitObject hitObject) - => hitObject is HitCircle || hitObject is Slider; + private static bool canBlockFutureHits(HitObject hitObject) + { + // Unlike the above we will receive slider tails, but they do not block future hits. + if (hitObject is SliderTailCircle) + return false; + + // All other hitcircles continue to block future hits. + return hitObject is HitCircle; + } } } From 42b3ff805b60c740fbd85948a8db366f8e91952b Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 10 Apr 2020 02:57:31 +0900 Subject: [PATCH 17/19] Rename methods + fix incorrect method usage --- osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs | 33 ++++++++------------ 1 file changed, 13 insertions(+), 20 deletions(-) diff --git a/osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs b/osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs index 0a09b5be7c..cfb850b785 100644 --- a/osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs +++ b/osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs @@ -42,7 +42,7 @@ namespace osu.Game.Rulesets.Osu.UI if (obj == hitObject) break; - if (canBlockFutureHits(obj)) + if (drawableCanBlockFutureHits(obj)) blockingObject = obj; } @@ -66,29 +66,26 @@ namespace osu.Game.Rulesets.Osu.UI /// The that was hit. public void HandleHit(HitObject hitObject) { - // Hitobjects which themselves don't block future hitobjects don't cause misses (e.g. slider ticks) - if (!canBlockFutureHits(hitObject)) + // Hitobjects which themselves don't block future hitobjects don't cause misses (e.g. slider ticks, spinners) + if (!hitObjectCanBlockFutureHits(hitObject)) return; double minimumTime = hitObject.StartTime; foreach (var obj in hitObjectContainer.AliveObjects) { - if (obj.HitObject.StartTime >= minimumTime) - break; - - // If the parent hitobject cannot cause a miss, neither can any nested hitobject. - if (!canBlockFutureHits(obj)) + if (obj.Judged || obj.HitObject.StartTime >= minimumTime) continue; - applyMiss(obj); + if (hitObjectCanBlockFutureHits(obj.HitObject)) + applyMiss(obj); foreach (var nested in obj.NestedHitObjects) { - if (nested.HitObject.StartTime >= minimumTime) - break; + if (nested.Judged || nested.HitObject.StartTime >= minimumTime) + continue; - if (canBlockFutureHits(nested)) + if (hitObjectCanBlockFutureHits(nested.HitObject)) applyMiss(nested); } } @@ -100,15 +97,11 @@ namespace osu.Game.Rulesets.Osu.UI /// Whether a blocks hits on future s until its start time is reached. /// /// - /// Must only be used when iterating through top-most drawable hitobjects. + /// This will ONLY match on top-most s. /// /// The to test. - private static bool canBlockFutureHits(DrawableHitObject hitObject) + private static bool drawableCanBlockFutureHits(DrawableHitObject hitObject) { - // Judged hitobjects can never block hits. - if (hitObject.Judged) - return false; - // Special considerations for slider tails aren't required since only top-most drawable hitobjects are being iterated over. return hitObject is DrawableHitCircle || hitObject is DrawableSlider; } @@ -117,10 +110,10 @@ namespace osu.Game.Rulesets.Osu.UI /// Whether a blocks hits on future s until its start time is reached. /// /// - /// Must only be used when iterating through nested hitobjects. + /// This is more rigorous and may not match on top-most s as does. /// /// The to test. - private static bool canBlockFutureHits(HitObject hitObject) + private static bool hitObjectCanBlockFutureHits(HitObject hitObject) { // Unlike the above we will receive slider tails, but they do not block future hits. if (hitObject is SliderTailCircle) From 15a92d1451c9fe0fb916f4c4e392c4da2411dcf3 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 10 Apr 2020 02:57:35 +0900 Subject: [PATCH 18/19] Rename test scene --- .../{TestSceneNoteLock.cs => TestSceneOutOfOrderHits.cs} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename osu.Game.Rulesets.Osu.Tests/{TestSceneNoteLock.cs => TestSceneOutOfOrderHits.cs} (99%) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneNoteLock.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneOutOfOrderHits.cs similarity index 99% rename from osu.Game.Rulesets.Osu.Tests/TestSceneNoteLock.cs rename to osu.Game.Rulesets.Osu.Tests/TestSceneOutOfOrderHits.cs index 2c69540951..d6858f831e 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneNoteLock.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneOutOfOrderHits.cs @@ -25,7 +25,7 @@ using osuTK; namespace osu.Game.Rulesets.Osu.Tests { - public class TestSceneNoteLock : RateAdjustedBeatmapTestScene + public class TestSceneOutOfOrderHits : RateAdjustedBeatmapTestScene { private const double early_miss_window = 1000; // time after -1000 to -500 is considered a miss private const double late_miss_window = 500; // time after +500 is considered a miss From 6988df30bd9cdfe7be77a21f6de63b11ca462a45 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 10 Apr 2020 03:12:13 +0900 Subject: [PATCH 19/19] Rename variable, add comment --- osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs b/osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs index cfb850b785..dfca2aff7b 100644 --- a/osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs +++ b/osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs @@ -66,15 +66,16 @@ namespace osu.Game.Rulesets.Osu.UI /// The that was hit. public void HandleHit(HitObject hitObject) { - // Hitobjects which themselves don't block future hitobjects don't cause misses (e.g. slider ticks, spinners) + // Hitobjects which themselves don't block future hitobjects don't cause misses (e.g. slider ticks, spinners). if (!hitObjectCanBlockFutureHits(hitObject)) return; - double minimumTime = hitObject.StartTime; + double maximumTime = hitObject.StartTime; + // Iterate through and apply miss results to all top-level and nested hitobjects which block future hits. foreach (var obj in hitObjectContainer.AliveObjects) { - if (obj.Judged || obj.HitObject.StartTime >= minimumTime) + if (obj.Judged || obj.HitObject.StartTime >= maximumTime) continue; if (hitObjectCanBlockFutureHits(obj.HitObject)) @@ -82,7 +83,7 @@ namespace osu.Game.Rulesets.Osu.UI foreach (var nested in obj.NestedHitObjects) { - if (nested.Judged || nested.HitObject.StartTime >= minimumTime) + if (nested.Judged || nested.HitObject.StartTime >= maximumTime) continue; if (hitObjectCanBlockFutureHits(nested.HitObject))