From 768d7b5e1c30329280447dc452fde063b6e440b3 Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Mon, 17 Jul 2023 23:31:21 -0400 Subject: [PATCH] correct implementation of stable notelock --- .../TestSceneHitCircle.cs | 2 +- .../Objects/Drawables/DrawableHitCircle.cs | 7 ++- .../Objects/Drawables/DrawableOsuHitObject.cs | 10 ++-- .../Objects/Drawables/DrawableSliderHead.cs | 3 +- osu.Game.Rulesets.Osu/UI/AnyOrderHitPolicy.cs | 2 +- osu.Game.Rulesets.Osu/UI/ClickAction.cs | 18 +++++++ osu.Game.Rulesets.Osu/UI/IHitPolicy.cs | 2 +- .../UI/ObjectOrderedHitPolicy.cs | 53 ++++++++++--------- osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs | 2 +- .../UI/StartTimeOrderedHitPolicy.cs | 8 +-- osu.Game/Rulesets/UI/HitObjectContainer.cs | 2 +- osu.Game/Rulesets/UI/IHitObjectContainer.cs | 2 +- 12 files changed, 72 insertions(+), 39 deletions(-) create mode 100644 osu.Game.Rulesets.Osu/UI/ClickAction.cs diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircle.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircle.cs index 0314afc1ac..c818a361df 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircle.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircle.cs @@ -131,7 +131,7 @@ namespace osu.Game.Rulesets.Osu.Tests protected override void CheckForResult(bool userTriggered, double timeOffset) { - if (auto && !userTriggered && timeOffset > hitOffset && CheckHittable?.Invoke(this, Time.Current) != false) + if (auto && !userTriggered && timeOffset > hitOffset && CheckHittable?.Invoke(this, Time.Current) == ClickAction.Hit) { // force success ApplyResult(r => r.Type = HitResult.Great); diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs index 3458069dd1..09d818def8 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs @@ -18,6 +18,7 @@ using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Rulesets.Osu.Skinning; using osu.Game.Rulesets.Osu.Skinning.Default; +using osu.Game.Rulesets.Osu.UI; using osu.Game.Rulesets.Scoring; using osu.Game.Skinning; using osuTK; @@ -154,13 +155,17 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables } var result = ResultFor(timeOffset); + var clickAction = CheckHittable?.Invoke(this, Time.Current); - if (result == HitResult.None || CheckHittable?.Invoke(this, Time.Current) == false) + if (clickAction == ClickAction.Shake || (result == HitResult.None && clickAction != ClickAction.Ignore)) { Shake(); return; } + if (result == HitResult.None) + return; + ApplyResult(r => { var circleResult = (OsuHitCircleJudgementResult)r; diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs index df0ba344d8..a8ce2118c8 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs @@ -12,6 +12,7 @@ using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Rulesets.Osu.Scoring; +using osu.Game.Rulesets.Osu.UI; using osuTK; using osuTK.Graphics; @@ -30,10 +31,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables protected override float SamplePlaybackPosition => CalculateDrawableRelativePosition(this); /// - /// Whether this can be hit, given a time value. - /// If non-null, judgements will be ignored (resulting in a shake) whilst the function returns false. + /// What action this should take in response to a + /// click at the given time value. + /// If non-null, judgements will be ignored for return values of + /// and , and this hit object will be shaken for return values of + /// . /// - public Func CheckHittable; + public Func CheckHittable; protected DrawableOsuHitObject(OsuHitObject hitObject) : base(hitObject) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs index b8a1efabe0..a4cf69ee31 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs @@ -8,6 +8,7 @@ using System.Diagnostics; using JetBrains.Annotations; using osu.Framework.Bindables; using osu.Game.Rulesets.Objects.Types; +using osu.Game.Rulesets.Osu.UI; using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Osu.Objects.Drawables @@ -60,7 +61,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables pathVersion.BindTo(DrawableSlider.PathVersion); - CheckHittable = (d, t) => DrawableSlider.CheckHittable?.Invoke(d, t) ?? true; + CheckHittable = (d, t) => DrawableSlider.CheckHittable?.Invoke(d, t) ?? ClickAction.Hit; } protected override void Update() diff --git a/osu.Game.Rulesets.Osu/UI/AnyOrderHitPolicy.cs b/osu.Game.Rulesets.Osu/UI/AnyOrderHitPolicy.cs index afa54c2dfb..7503c43e0b 100644 --- a/osu.Game.Rulesets.Osu/UI/AnyOrderHitPolicy.cs +++ b/osu.Game.Rulesets.Osu/UI/AnyOrderHitPolicy.cs @@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Osu.UI { public IHitObjectContainer HitObjectContainer { get; set; } - public bool IsHittable(DrawableHitObject hitObject, double time) => true; + public ClickAction CheckHittable(DrawableHitObject hitObject, double time) => ClickAction.Hit; public void HandleHit(DrawableHitObject hitObject) { diff --git a/osu.Game.Rulesets.Osu/UI/ClickAction.cs b/osu.Game.Rulesets.Osu/UI/ClickAction.cs new file mode 100644 index 0000000000..2b00f5acce --- /dev/null +++ b/osu.Game.Rulesets.Osu/UI/ClickAction.cs @@ -0,0 +1,18 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Rulesets.Osu.Objects.Drawables; + +namespace osu.Game.Rulesets.Osu.UI +{ + /// + /// An action that an recommends be taken in response to a click + /// on a . + /// + public enum ClickAction + { + Ignore, + Shake, + Hit + } +} diff --git a/osu.Game.Rulesets.Osu/UI/IHitPolicy.cs b/osu.Game.Rulesets.Osu/UI/IHitPolicy.cs index b509796742..9820b8c188 100644 --- a/osu.Game.Rulesets.Osu/UI/IHitPolicy.cs +++ b/osu.Game.Rulesets.Osu/UI/IHitPolicy.cs @@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Osu.UI /// The to check. /// The time to check. /// Whether can be hit at the given . - bool IsHittable(DrawableHitObject hitObject, double time); + ClickAction CheckHittable(DrawableHitObject hitObject, double time); /// /// Handles a being hit. diff --git a/osu.Game.Rulesets.Osu/UI/ObjectOrderedHitPolicy.cs b/osu.Game.Rulesets.Osu/UI/ObjectOrderedHitPolicy.cs index 6330208d37..07942954e1 100644 --- a/osu.Game.Rulesets.Osu/UI/ObjectOrderedHitPolicy.cs +++ b/osu.Game.Rulesets.Osu/UI/ObjectOrderedHitPolicy.cs @@ -3,8 +3,7 @@ #nullable disable -using System.Collections.Generic; -using System.Linq; +using System; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects.Drawables; @@ -22,35 +21,41 @@ namespace osu.Game.Rulesets.Osu.UI { public IHitObjectContainer HitObjectContainer { get; set; } - public bool IsHittable(DrawableHitObject hitObject, double time) => enumerateHitObjectsUpTo(hitObject.HitObject.StartTime).All(obj => obj.AllJudged); - public void HandleHit(DrawableHitObject hitObject) { } - private IEnumerable enumerateHitObjectsUpTo(double targetTime) + public ClickAction CheckHittable(DrawableHitObject hitObject, double time) { - foreach (var obj in HitObjectContainer.AliveObjects) + int index = HitObjectContainer.AliveObjects.IndexOf(hitObject); + + if (index > 0) { - if (obj.HitObject.StartTime >= targetTime) - yield break; - - switch (obj) - { - case DrawableSpinner: - continue; - - case DrawableSlider slider: - yield return slider.HeadCircle; - - break; - - default: - yield return obj; - - break; - } + var previousHitObject = (DrawableOsuHitObject)HitObjectContainer.AliveObjects[index - 1]; + if (previousHitObject.HitObject.StackHeight > 0 && !previousHitObject.AllJudged) + return ClickAction.Ignore; } + + foreach (DrawableHitObject testObject in HitObjectContainer.AliveObjects) + { + if (testObject.AllJudged) + continue; + + // if we found the object being checked, we can move on to the final timing test. + if (testObject == hitObject) + break; + + // for all other objects, we check for validity and block the hit if any are still valid. + // 3ms of extra leniency to account for slightly unsnapped objects. + if (testObject.HitObject.GetEndTime() + 3 < hitObject.HitObject.StartTime) + return ClickAction.Shake; + } + + // stable has `const HitObjectManager.HITTABLE_RANGE = 400;`, which is only used for notelock code. + // probably not a coincidence that this is equivalent to lazer's OsuHitWindows.MISS_WINDOW. + + // TODO stable compares to 200 when autopilot is enabled, instead of 400. + return Math.Abs(hitObject.HitObject.StartTime - time) < 400 ? ClickAction.Hit : ClickAction.Shake; } } } diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs index ed02284a4b..15ca0a90de 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs @@ -89,7 +89,7 @@ namespace osu.Game.Rulesets.Osu.UI protected override void OnNewDrawableHitObject(DrawableHitObject drawable) { - ((DrawableOsuHitObject)drawable).CheckHittable = hitPolicy.IsHittable; + ((DrawableOsuHitObject)drawable).CheckHittable = hitPolicy.CheckHittable; Debug.Assert(!drawable.IsLoaded, $"Already loaded {nameof(DrawableHitObject)} is added to {nameof(OsuPlayfield)}"); drawable.OnLoadComplete += onDrawableHitObjectLoaded; diff --git a/osu.Game.Rulesets.Osu/UI/StartTimeOrderedHitPolicy.cs b/osu.Game.Rulesets.Osu/UI/StartTimeOrderedHitPolicy.cs index edc3ba0818..f33ca58aef 100644 --- a/osu.Game.Rulesets.Osu/UI/StartTimeOrderedHitPolicy.cs +++ b/osu.Game.Rulesets.Osu/UI/StartTimeOrderedHitPolicy.cs @@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Osu.UI { public IHitObjectContainer HitObjectContainer { get; set; } - public bool IsHittable(DrawableHitObject hitObject, double time) + public ClickAction CheckHittable(DrawableHitObject hitObject, double time) { DrawableHitObject blockingObject = null; @@ -36,13 +36,13 @@ namespace osu.Game.Rulesets.Osu.UI // If there is no previous hitobject, allow the hit. if (blockingObject == null) - return true; + return ClickAction.Hit; // 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). - return blockingObject.Judged || time >= blockingObject.HitObject.StartTime; + return (blockingObject.Judged || time >= blockingObject.HitObject.StartTime) ? ClickAction.Hit : ClickAction.Shake; } public void HandleHit(DrawableHitObject hitObject) @@ -51,7 +51,7 @@ namespace osu.Game.Rulesets.Osu.UI if (!hitObjectCanBlockFutureHits(hitObject)) return; - if (!IsHittable(hitObject, hitObject.HitObject.StartTime + hitObject.Result.TimeOffset)) + if (CheckHittable(hitObject, hitObject.HitObject.StartTime + hitObject.Result.TimeOffset) != ClickAction.Hit) throw new InvalidOperationException($"A {hitObject} was hit before it became hittable!"); // Miss all hitobjects prior to the hit one. diff --git a/osu.Game/Rulesets/UI/HitObjectContainer.cs b/osu.Game/Rulesets/UI/HitObjectContainer.cs index 099be486b3..454a83bcda 100644 --- a/osu.Game/Rulesets/UI/HitObjectContainer.cs +++ b/osu.Game/Rulesets/UI/HitObjectContainer.cs @@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.UI { public IEnumerable Objects => InternalChildren.Cast().OrderBy(h => h.HitObject.StartTime); - public IEnumerable AliveObjects => AliveEntries.Select(pair => pair.Drawable).OrderBy(h => h.HitObject.StartTime); + public IList AliveObjects => AliveEntries.Select(pair => pair.Drawable).OrderBy(h => h.HitObject.StartTime).ToList(); /// /// Invoked when a is judged. diff --git a/osu.Game/Rulesets/UI/IHitObjectContainer.cs b/osu.Game/Rulesets/UI/IHitObjectContainer.cs index 6dcb0944be..bb4806206d 100644 --- a/osu.Game/Rulesets/UI/IHitObjectContainer.cs +++ b/osu.Game/Rulesets/UI/IHitObjectContainer.cs @@ -19,6 +19,6 @@ namespace osu.Game.Rulesets.UI /// /// If this uses pooled objects, this is equivalent to . /// - IEnumerable AliveObjects { get; } + IList AliveObjects { get; } } }