1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-13 08:32:57 +08:00

correct implementation of stable notelock

This commit is contained in:
Liam DeVoe 2023-07-17 23:31:21 -04:00
parent 88ac53557a
commit 768d7b5e1c
12 changed files with 72 additions and 39 deletions

View File

@ -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);

View File

@ -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;

View File

@ -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);
/// <summary>
/// Whether this <see cref="DrawableOsuHitObject"/> 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 <see cref="DrawableOsuHitObject"/> should take in response to a
/// click at the given time value.
/// If non-null, judgements will be ignored for return values of <see cref="ClickAction.Ignore"/>
/// and <see cref="ClickAction.Shake"/>, and this hit object will be shaken for return values of
/// <see cref="ClickAction.Shake"/>.
/// </summary>
public Func<DrawableHitObject, double, bool> CheckHittable;
public Func<DrawableHitObject, double, ClickAction> CheckHittable;
protected DrawableOsuHitObject(OsuHitObject hitObject)
: base(hitObject)

View File

@ -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()

View File

@ -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)
{

View File

@ -0,0 +1,18 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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
{
/// <summary>
/// An action that an <see cref="IHitPolicy"/> recommends be taken in response to a click
/// on a <see cref="DrawableOsuHitObject"/>.
/// </summary>
public enum ClickAction
{
Ignore,
Shake,
Hit
}
}

View File

@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Osu.UI
/// <param name="hitObject">The <see cref="DrawableHitObject"/> to check.</param>
/// <param name="time">The time to check.</param>
/// <returns>Whether <paramref name="hitObject"/> can be hit at the given <paramref name="time"/>.</returns>
bool IsHittable(DrawableHitObject hitObject, double time);
ClickAction CheckHittable(DrawableHitObject hitObject, double time);
/// <summary>
/// Handles a <see cref="HitObject"/> being hit.

View File

@ -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<DrawableHitObject> 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;
}
}
}

View File

@ -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;

View File

@ -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.

View File

@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.UI
{
public IEnumerable<DrawableHitObject> Objects => InternalChildren.Cast<DrawableHitObject>().OrderBy(h => h.HitObject.StartTime);
public IEnumerable<DrawableHitObject> AliveObjects => AliveEntries.Select(pair => pair.Drawable).OrderBy(h => h.HitObject.StartTime);
public IList<DrawableHitObject> AliveObjects => AliveEntries.Select(pair => pair.Drawable).OrderBy(h => h.HitObject.StartTime).ToList();
/// <summary>
/// Invoked when a <see cref="DrawableHitObject"/> is judged.

View File

@ -19,6 +19,6 @@ namespace osu.Game.Rulesets.UI
/// <remarks>
/// If this <see cref="IHitObjectContainer"/> uses pooled objects, this is equivalent to <see cref="Objects"/>.
/// </remarks>
IEnumerable<DrawableHitObject> AliveObjects { get; }
IList<DrawableHitObject> AliveObjects { get; }
}
}