Refactor all slider input into SliderInputManager

This commit is contained in:
Dan Balasescu 2023-12-15 16:13:32 +09:00
parent 599fdb0128
commit 6bd190c55d
No known key found for this signature in database
10 changed files with 270 additions and 307 deletions

View File

@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
public partial class DrawableHitCircle : DrawableOsuHitObject, IHasApproachCircle
{
public OsuAction? HitAction => HitArea.HitAction;
public OsuAction? HitAction => HitArea?.HitAction;
protected virtual OsuSkinComponents CirclePieceComponent => OsuSkinComponents.HitCircle;
public SkinnableDrawable ApproachCircle { get; private set; }

View File

@ -97,6 +97,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
/// </summary>
public virtual void Shake() { }
/// <summary>
/// Causes this <see cref="DrawableOsuHitObject"/> to get hit, disregarding all conditions in implementations of <see cref="DrawableHitObject.CheckForResult"/>.
/// </summary>
public void HitForcefully() => ApplyResult(r => r.Type = r.Judgement.MaxResult);
/// <summary>
/// Causes this <see cref="DrawableOsuHitObject"/> to get missed, disregarding all conditions in implementations of <see cref="DrawableHitObject.CheckForResult"/>.
/// </summary>

View File

@ -28,8 +28,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
public DrawableSliderHead HeadCircle => headContainer.Child;
public DrawableSliderTail TailCircle => tailContainer.Child;
public IEnumerable<DrawableSliderTick> Ticks => tickContainer.Children;
public IEnumerable<DrawableSliderRepeat> Repeats => repeatContainer.Children;
[Cached]
public DrawableSliderBall Ball { get; private set; }
@ -60,6 +58,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
public IBindable<int> PathVersion => pathVersion;
private readonly Bindable<int> pathVersion = new Bindable<int>();
public readonly SliderInputManager SliderInputManager;
private Container<DrawableSliderHead> headContainer;
private Container<DrawableSliderTail> tailContainer;
private Container<DrawableSliderTick> tickContainer;
@ -74,9 +74,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
public DrawableSlider([CanBeNull] Slider s = null)
: base(s)
{
SliderInputManager = new SliderInputManager(this);
Ball = new DrawableSliderBall
{
GetInitialHitAction = () => HeadCircle.HitAction,
BypassAutoSizeAxes = Axes.Both,
AlwaysPresent = true,
Alpha = 0
@ -90,6 +91,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
AddRangeInternal(new Drawable[]
{
SliderInputManager,
shakeContainer = new ShakeContainer
{
ShakeDuration = 30,
@ -234,7 +236,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
base.Update();
Tracking.Value = Ball.Tracking;
Tracking.Value = SliderInputManager.Tracking;
if (Tracking.Value && slidingSample != null)
// keep the sliding sample playing at the current tracking position
@ -247,8 +249,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
foreach (DrawableHitObject hitObject in NestedHitObjects)
{
if (hitObject is ITrackSnaking s) s.UpdateSnakingPosition(HitObject.Path.PositionAt(SliderBody?.SnakedStart ?? 0), HitObject.Path.PositionAt(SliderBody?.SnakedEnd ?? 0));
if (hitObject is IRequireTracking t) t.Tracking = Ball.Tracking;
if (hitObject is ITrackSnaking s)
s.UpdateSnakingPosition(HitObject.Path.PositionAt(SliderBody?.SnakedStart ?? 0), HitObject.Path.PositionAt(SliderBody?.SnakedEnd ?? 0));
}
Size = SliderBody?.Size ?? Vector2.Zero;

View File

@ -4,14 +4,9 @@
#nullable disable
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input;
using osu.Framework.Input.Events;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Skinning.Default;
@ -21,12 +16,10 @@ using osuTK;
namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
public partial class DrawableSliderBall : CircularContainer, ISliderProgress, IRequireHighFrequencyMousePosition
public partial class DrawableSliderBall : CircularContainer, ISliderProgress
{
public const float FOLLOW_AREA = 2.4f;
public Func<OsuAction?> GetInitialHitAction;
private DrawableSlider drawableSlider;
private Drawable ball;
@ -55,14 +48,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
};
}
private Vector2? lastScreenSpaceMousePosition;
protected override bool OnMouseMove(MouseMoveEvent e)
{
lastScreenSpaceMousePosition = e.ScreenSpaceMousePosition;
return base.OnMouseMove(e);
}
public override void ClearTransformsAfter(double time, bool propagateChildren = false, string targetMember = null)
{
// Consider the case of rewinding - children's transforms are handled internally, so propagating down
@ -78,115 +63,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
base.ApplyTransformsAt(time, false);
}
public bool Tracking { get; private set; }
/// <summary>
/// If the cursor moves out of the ball's radius we still need to be able to receive positional updates to stop tracking.
/// </summary>
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true;
/// <summary>
/// The point in time after which we can accept any key for tracking. Before this time, we may need to restrict tracking to the key used to hit the head circle.
///
/// This is a requirement to stop the case where a player holds down one key (from before the slider) and taps the second key while maintaining full scoring (tracking) of sliders.
/// Visually, this special case can be seen below (time increasing from left to right):
///
/// Z Z+X Z
/// o========o
///
/// Without this logic, tracking would continue through the entire slider even though no key hold action is directly attributing to it.
///
/// In all other cases, no special handling is required (either key being pressed is allowable as valid tracking).
///
/// The reason for storing this as a time value (rather than a bool) is to correctly handle rewind scenarios.
/// </summary>
private double? timeToAcceptAnyKeyAfter;
/// <summary>
/// The actions that were pressed in the previous frame.
/// </summary>
private readonly List<OsuAction> lastPressedActions = new List<OsuAction>();
public bool IsMouseInFollowCircleWithState(bool expanded)
{
if (lastScreenSpaceMousePosition is not Vector2 mousePos)
return false;
float radius = GetFollowCircleRadius(expanded);
double followProgress = Math.Clamp((Time.Current - drawableSlider.HitObject.StartTime) / drawableSlider.HitObject.Duration, 0, 1);
Vector2 followCirclePosition = drawableSlider.HitObject.CurvePositionAt(followProgress);
Vector2 mousePositionInSlider = drawableSlider.ToLocalSpace(mousePos) - drawableSlider.OriginPosition;
return (mousePositionInSlider - followCirclePosition).LengthSquared <= radius * radius;
}
public float GetFollowCircleRadius(bool expanded)
{
float radius = (float)drawableSlider.HitObject.Radius;
if (expanded)
radius *= FOLLOW_AREA;
return radius;
}
protected override void Update()
{
base.Update();
// from the point at which the head circle is hit, this will be non-null.
// it may be null if the head circle was missed.
var headCircleHitAction = GetInitialHitAction();
if (headCircleHitAction == null)
timeToAcceptAnyKeyAfter = null;
var actions = drawableSlider.OsuActionInputManager?.PressedActions;
// if the head circle was hit with a specific key, tracking should only occur while that key is pressed.
if (headCircleHitAction != null && timeToAcceptAnyKeyAfter == null)
{
var otherKey = headCircleHitAction == OsuAction.RightButton ? OsuAction.LeftButton : OsuAction.RightButton;
// we can start accepting any key once all other keys have been released in the previous frame.
if (!lastPressedActions.Contains(otherKey))
timeToAcceptAnyKeyAfter = Time.Current;
}
bool validInFollowArea = IsMouseInFollowCircleWithState(Tracking);
bool validInHeadCircle = drawableSlider.HeadCircle.IsHit
&& IsMouseInFollowCircleWithState(true)
&& drawableSlider.HeadCircle.Result.TimeAbsolute == Time.Current;
Tracking =
// even in an edge case where current time has exceeded the slider's time, we may not have finished judging.
// we don't want to potentially update from Tracking=true to Tracking=false at this point.
(!drawableSlider.AllJudged || Time.Current <= drawableSlider.HitObject.GetEndTime())
// in valid position range
&& (validInFollowArea || validInHeadCircle)
// valid action
&& (actions?.Any(isValidTrackingAction) ?? false);
lastPressedActions.Clear();
if (actions != null)
lastPressedActions.AddRange(actions);
}
/// <summary>
/// Check whether a given user input is a valid tracking action.
/// </summary>
private bool isValidTrackingAction(OsuAction action)
{
bool headCircleHit = GetInitialHitAction().HasValue;
// if the head circle was hit, we may not yet be allowed to accept any key, so we must use the initial hit action.
if (headCircleHit && (!timeToAcceptAnyKeyAfter.HasValue || Time.Current <= timeToAcceptAnyKeyAfter.Value))
return action == GetInitialHitAction();
return action == OsuAction.LeftButton || action == OsuAction.RightButton;
}
private Vector2? lastPosition;
public void UpdateProgress(double completionProgress)

View File

@ -3,14 +3,10 @@
#nullable disable
using System;
using System.Diagnostics;
using System.Linq;
using osu.Framework.Bindables;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.Scoring;
using osuTK;
namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
@ -68,45 +64,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
protected override void CheckForResult(bool userTriggered, double timeOffset)
{
base.CheckForResult(userTriggered, timeOffset);
if (!Judged || !Result.IsHit)
return;
// If the head is hit and in radius of the would-be-expanded follow circle,
// then hit every object that the follow circle has passed through up until the current time.
if (DrawableSlider.Ball.IsMouseInFollowCircleWithState(true))
{
foreach (var nested in DrawableSlider.NestedHitObjects.OfType<DrawableOsuHitObject>())
{
if (nested.Judged)
continue;
if (!check(nested.HitObject))
break;
if (nested is DrawableSliderTick tick)
tick.HitForcefully();
if (nested is DrawableSliderRepeat repeat)
repeat.HitForcefully();
if (nested is DrawableSliderTail tail)
tail.HitForcefully();
}
}
bool check(OsuHitObject h)
{
if (h.StartTime > Time.Current)
return false;
float radius = DrawableSlider.Ball.GetFollowCircleRadius(true);
double objectProgress = Math.Clamp((h.StartTime - DrawableSlider.HitObject.StartTime) / DrawableSlider.HitObject.Duration, 0, 1);
Vector2 objectPosition = DrawableSlider.HitObject.CurvePositionAt(objectProgress);
return objectPosition.LengthSquared <= radius * radius;
}
DrawableSlider.SliderInputManager.PostProcessHeadJudgement(this);
}
protected override HitResult ResultFor(double timeOffset)

View File

@ -17,7 +17,7 @@ using osuTK;
namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
public partial class DrawableSliderRepeat : DrawableOsuHitObject, ITrackSnaking, IRequireTracking
public partial class DrawableSliderRepeat : DrawableOsuHitObject, ITrackSnaking
{
public new SliderRepeat HitObject => (SliderRepeat)base.HitObject;
@ -36,8 +36,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
public override bool DisplayResult => false;
public bool Tracking { get; set; }
public DrawableSliderRepeat()
: base(null)
{
@ -85,37 +83,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
Position = HitObject.Position - DrawableSlider.Position;
}
public void HitForcefully()
{
if (Judged)
return;
ApplyResult(r => r.Type = r.Judgement.MaxResult);
}
protected override void CheckForResult(bool userTriggered, double timeOffset)
{
// shared implementation with DrawableSliderTick.
if (timeOffset >= 0)
{
// This check is applied to all nested slider objects apart from the head (ticks, repeats, tail).
if (!DrawableSlider.HeadCircle.Judged)
{
if (Tracking)
{
// Attempt to preserve correct ordering of judgements as best we can by forcing an un-judged head to be missed when the user has clearly skipped it.
DrawableSlider.HeadCircle.MissForcefully();
}
else
{
// Don't judge this object as a miss before the head has been judged, to allow the head to be hit late.
return;
}
}
ApplyResult(r => r.Type = Tracking ? r.Judgement.MaxResult : r.Judgement.MinResult);
}
}
protected override void CheckForResult(bool userTriggered, double timeOffset) => DrawableSlider.SliderInputManager.TryJudgeNestedObject(this, timeOffset);
protected override void UpdateInitialTransforms()
{

View File

@ -4,12 +4,10 @@
#nullable disable
using System.Diagnostics;
using System.Linq;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Skinning;
@ -17,7 +15,7 @@ using osuTK;
namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
public partial class DrawableSliderTail : DrawableOsuHitObject, IRequireTracking
public partial class DrawableSliderTail : DrawableOsuHitObject
{
public new SliderTailCircle HitObject => (SliderTailCircle)base.HitObject;
@ -37,8 +35,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
/// </summary>
public bool SamplePlaysOnlyOnHit { get; set; } = true;
public bool Tracking { get; set; }
public SkinnableDrawable CirclePiece { get; private set; }
private Container scaleContainer;
@ -125,52 +121,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
}
}
public void HitForcefully()
{
if (Judged)
return;
ApplyResult(r => r.Type = r.Judgement.MaxResult);
}
protected override void CheckForResult(bool userTriggered, double timeOffset)
{
if (userTriggered)
return;
// Ensure the tail can only activate after all previous ticks/repeats already have.
//
// This covers the edge case where the lenience may allow the tail to activate before
// the last tick, changing ordering of score/combo awarding.
var lastTick = DrawableSlider.NestedHitObjects.LastOrDefault(o => o.HitObject is SliderTick || o.HitObject is SliderRepeat);
if (lastTick?.Judged == false)
return;
if (timeOffset < SliderEventGenerator.TAIL_LENIENCY)
return;
// This check is applied to all nested slider objects apart from the head (ticks, repeats, tail).
if (!DrawableSlider.HeadCircle.Judged)
{
if (Tracking)
{
// Attempt to preserve correct ordering of judgements as best we can by forcing an un-judged head to be missed when the user has clearly skipped it.
DrawableSlider.HeadCircle.MissForcefully();
}
else
{
// Don't judge this object as a miss before the head has been judged, to allow the head to be hit late.
return;
}
}
// The player needs to have engaged in tracking at any point after the tail leniency cutoff.
// An actual tick miss should only occur if reaching the tick itself.
if (Tracking)
ApplyResult(r => r.Type = r.Judgement.MaxResult);
else if (timeOffset > 0)
ApplyResult(r => r.Type = r.Judgement.MinResult);
}
protected override void CheckForResult(bool userTriggered, double timeOffset) => DrawableSlider.SliderInputManager.TryJudgeNestedObject(this, timeOffset);
protected override void OnApply()
{

View File

@ -14,14 +14,12 @@ using osu.Framework.Graphics.Containers;
namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
public partial class DrawableSliderTick : DrawableOsuHitObject, IRequireTracking
public partial class DrawableSliderTick : DrawableOsuHitObject
{
public const double ANIM_DURATION = 150;
private const float default_tick_size = 16;
public bool Tracking { get; set; }
public override bool DisplayResult => false;
protected DrawableSlider DrawableSlider => (DrawableSlider)ParentHitObject;
@ -73,37 +71,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
Position = HitObject.Position - DrawableSlider.HitObject.Position;
}
public void HitForcefully()
{
if (Judged)
return;
ApplyResult(r => r.Type = r.Judgement.MaxResult);
}
protected override void CheckForResult(bool userTriggered, double timeOffset)
{
// shared implementation with DrawableSliderRepeat.
if (timeOffset >= 0)
{
// This check is applied to all nested slider objects apart from the head (ticks, repeats, tail).
if (!DrawableSlider.HeadCircle.Judged)
{
if (Tracking)
{
// Attempt to preserve correct ordering of judgements as best we can by forcing an un-judged head to be missed when the user has clearly skipped it.
DrawableSlider.HeadCircle.MissForcefully();
}
else
{
// Don't judge this object as a miss before the head has been judged, to allow the head to be hit late.
return;
}
}
ApplyResult(r => r.Type = Tracking ? r.Judgement.MaxResult : r.Judgement.MinResult);
}
}
protected override void CheckForResult(bool userTriggered, double timeOffset) => DrawableSlider.SliderInputManager.TryJudgeNestedObject(this, timeOffset);
protected override void UpdateInitialTransforms()
{

View File

@ -1,13 +0,0 @@
// 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.
namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
public interface IRequireTracking
{
/// <summary>
/// Whether the <see cref="DrawableSlider"/> is currently being tracked by the user.
/// </summary>
bool Tracking { get; set; }
}
}

View File

@ -0,0 +1,248 @@
// 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 System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Graphics;
using osu.Framework.Input;
using osu.Framework.Input.Events;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osuTK;
namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
public partial class SliderInputManager : Component, IRequireHighFrequencyMousePosition
{
/// <summary>
/// Whether the slider is currently being tracked.
/// </summary>
public bool Tracking { get; private set; }
/// <summary>
/// The point in time after which we can accept any key for tracking. Before this time, we may need to restrict tracking to the key used to hit the head circle.
///
/// This is a requirement to stop the case where a player holds down one key (from before the slider) and taps the second key while maintaining full scoring (tracking) of sliders.
/// Visually, this special case can be seen below (time increasing from left to right):
///
/// Z Z+X Z
/// o========o
///
/// Without this logic, tracking would continue through the entire slider even though no key hold action is directly attributing to it.
///
/// In all other cases, no special handling is required (either key being pressed is allowable as valid tracking).
///
/// The reason for storing this as a time value (rather than a bool) is to correctly handle rewind scenarios.
/// </summary>
private double? timeToAcceptAnyKeyAfter;
/// <summary>
/// The actions that were pressed in the previous frame.
/// </summary>
private readonly List<OsuAction> lastPressedActions = new List<OsuAction>();
private Vector2? screenSpaceMousePosition;
private readonly DrawableSlider slider;
public SliderInputManager(DrawableSlider slider)
{
this.slider = slider;
}
/// <summary>
/// This component handles all input of the slider, so it should receive input no matter the position.
/// </summary>
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true;
protected override bool OnMouseMove(MouseMoveEvent e)
{
screenSpaceMousePosition = e.ScreenSpaceMousePosition;
return base.OnMouseMove(e);
}
protected override void Update()
{
base.Update();
updateTracking(isMouseInFollowArea(Tracking));
}
public void PostProcessHeadJudgement(DrawableSliderHead head)
{
if (!head.Judged || !head.Result.IsHit)
return;
if (!isMouseInFollowArea(true))
return;
// When the head is hit and the mouse is in the expanded follow area, force a hit on every nested hitobject
// from the start of the slider that is within follow-radius units from the head.
bool forceMiss = false;
foreach (var nested in slider.NestedHitObjects.OfType<DrawableOsuHitObject>())
{
// Skip nested objects that are already judged.
if (nested.Judged)
continue;
// Stop the process when a nested object is reached that can't be hit before the current time.
if (nested.HitObject.StartTime > Time.Current)
break;
float radius = getFollowRadius(true);
double objectProgress = Math.Clamp((nested.HitObject.StartTime - slider.HitObject.StartTime) / slider.HitObject.Duration, 0, 1);
Vector2 objectPosition = slider.HitObject.CurvePositionAt(objectProgress);
// When the first nested object that is further than follow-radius units away from the start of the slider is reached,
// forcefully miss all other nested objects that would otherwise be valid to be hit by this process.
if (forceMiss || objectPosition.LengthSquared > radius * radius)
{
nested.MissForcefully();
forceMiss = true;
}
else
nested.HitForcefully();
}
// Enable tracking, since the mouse is within the follow area (if it were expanded).
updateTracking(true);
}
public void TryJudgeNestedObject(DrawableOsuHitObject nestedObject, double timeOffset)
{
switch (nestedObject)
{
case DrawableSliderRepeat:
case DrawableSliderTick:
if (timeOffset < 0)
return;
break;
case DrawableSliderTail:
if (timeOffset < SliderEventGenerator.TAIL_LENIENCY)
return;
// Ensure the tail can only activate after all previous ticks/repeats already have.
//
// This covers the edge case where the lenience may allow the tail to activate before
// the last tick, changing ordering of score/combo awarding.
var lastTick = slider.NestedHitObjects.LastOrDefault(o => o.HitObject is SliderTick || o.HitObject is SliderRepeat);
if (lastTick?.Judged == false)
return;
break;
default:
return;
}
if (!slider.HeadCircle.Judged)
{
if (slider.Tracking.Value)
{
// Attempt to preserve correct ordering of judgements as best we can by forcing an un-judged head to be missed when the user has clearly skipped it.
slider.HeadCircle.MissForcefully();
}
else
{
// Don't judge this object as a miss before the head has been judged, to allow the head to be hit late.
return;
}
}
if (slider.Tracking.Value)
nestedObject.HitForcefully();
else
nestedObject.MissForcefully();
}
/// <summary>
/// Whether the mouse is currently in the follow area.
/// </summary>
/// <param name="expanded">Whether to test against the maximum area of the follow circle.</param>
private bool isMouseInFollowArea(bool expanded)
{
if (screenSpaceMousePosition is not Vector2 pos)
return false;
float radius = getFollowRadius(expanded);
double followProgress = Math.Clamp((Time.Current - slider.HitObject.StartTime) / slider.HitObject.Duration, 0, 1);
Vector2 followCirclePosition = slider.HitObject.CurvePositionAt(followProgress);
Vector2 mousePositionInSlider = slider.ToLocalSpace(pos) - slider.OriginPosition;
return (mousePositionInSlider - followCirclePosition).LengthSquared <= radius * radius;
}
/// <summary>
/// Retrieves the radius of the follow area.
/// </summary>
/// <param name="expanded">Whether to return the maximum area of the follow circle.</param>
private float getFollowRadius(bool expanded)
{
float radius = (float)slider.HitObject.Radius;
if (expanded)
radius *= DrawableSliderBall.FOLLOW_AREA;
return radius;
}
/// <summary>
/// Updates the tracking state.
/// </summary>
/// <param name="isValidTrackingPosition">Whether the current mouse position is valid to begin tracking.</param>
private void updateTracking(bool isValidTrackingPosition)
{
// from the point at which the head circle is hit, this will be non-null.
// it may be null if the head circle was missed.
OsuAction? headCircleHitAction = getInitialHitAction();
if (headCircleHitAction == null)
timeToAcceptAnyKeyAfter = null;
var actions = slider.OsuActionInputManager?.PressedActions;
// if the head circle was hit with a specific key, tracking should only occur while that key is pressed.
if (headCircleHitAction != null && timeToAcceptAnyKeyAfter == null)
{
var otherKey = headCircleHitAction == OsuAction.RightButton ? OsuAction.LeftButton : OsuAction.RightButton;
// we can start accepting any key once all other keys have been released in the previous frame.
if (!lastPressedActions.Contains(otherKey))
timeToAcceptAnyKeyAfter = Time.Current;
}
Tracking =
// even in an edge case where current time has exceeded the slider's time, we may not have finished judging.
// we don't want to potentially update from Tracking=true to Tracking=false at this point.
(!slider.AllJudged || Time.Current <= slider.HitObject.GetEndTime())
// in valid position range
&& isValidTrackingPosition
// valid action
&& (actions?.Any(isValidTrackingAction) ?? false);
lastPressedActions.Clear();
if (actions != null)
lastPressedActions.AddRange(actions);
}
private OsuAction? getInitialHitAction() => slider.HeadCircle?.HitAction;
/// <summary>
/// Check whether a given user input is a valid tracking action.
/// </summary>
private bool isValidTrackingAction(OsuAction action)
{
OsuAction? hitAction = getInitialHitAction();
// if the head circle was hit, we may not yet be allowed to accept any key, so we must use the initial hit action.
if (hitAction.HasValue && (!timeToAcceptAnyKeyAfter.HasValue || Time.Current <= timeToAcceptAnyKeyAfter.Value))
return action == hitAction;
return action == OsuAction.LeftButton || action == OsuAction.RightButton;
}
}
}