2019-03-08 14:14:57 +08:00
// 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 ;
2019-08-01 16:31:26 +08:00
using System.Linq ;
2020-05-15 17:17:39 +08:00
using System.Threading ;
2019-03-08 14:14:57 +08:00
namespace osu.Game.Rulesets.Objects
{
public static class SliderEventGenerator
{
2023-09-29 13:19:26 +08:00
/// <summary>
/// Historically, slider's final tick (aka the place where the slider would receive a final judgement) was offset by -36 ms. Originally this was
/// done to workaround a technical detail (unimportant), but over the years it has become an expectation of players that you don't need to hold
/// until the true end of the slider. This very small amount of leniency makes it easier to jump away from fast sliders to the next hit object.
///
/// After discussion on how this should be handled going forward, players have unanimously stated that this lenience should remain in some way.
2023-09-29 15:44:04 +08:00
/// These days, this is implemented in the drawable implementation of Slider in the osu! ruleset.
///
/// We need to keep the <see cref="SliderEventType.LegacyLastTick"/> *only* for osu!catch conversion, which relies on it to generate tiny ticks
/// correctly.
2023-09-29 13:19:26 +08:00
/// </summary>
2023-09-29 15:44:04 +08:00
public const double TAIL_LENIENCY = - 36 ;
2023-09-29 13:19:26 +08:00
2019-08-01 16:31:26 +08:00
public static IEnumerable < SliderEventDescriptor > Generate ( double startTime , double spanDuration , double velocity , double tickDistance , double totalDistance , int spanCount ,
2023-09-29 13:19:26 +08:00
CancellationToken cancellationToken = default )
2019-03-08 14:14:57 +08:00
{
// A very lenient maximum length of a slider for ticks to be generated.
// This exists for edge cases such as /b/1573664 where the beatmap has been edited by the user, and should never be reached in normal usage.
const double max_length = 100000 ;
2021-10-27 12:04:41 +08:00
double length = Math . Min ( max_length , totalDistance ) ;
2019-11-20 20:19:49 +08:00
tickDistance = Math . Clamp ( tickDistance , 0 , length ) ;
2019-03-08 14:14:57 +08:00
2021-10-27 12:04:41 +08:00
double minDistanceFromEnd = velocity * 10 ;
2019-03-08 14:14:57 +08:00
yield return new SliderEventDescriptor
{
Type = SliderEventType . Head ,
SpanIndex = 0 ,
SpanStartTime = startTime ,
2019-03-11 13:36:29 +08:00
Time = startTime ,
2019-03-08 14:14:57 +08:00
PathProgress = 0 ,
} ;
if ( tickDistance ! = 0 )
{
2021-10-27 12:04:41 +08:00
for ( int span = 0 ; span < spanCount ; span + + )
2019-03-08 14:14:57 +08:00
{
2021-10-27 12:04:41 +08:00
double spanStartTime = startTime + span * spanDuration ;
bool reversed = span % 2 = = 1 ;
2019-03-08 14:14:57 +08:00
2020-05-15 17:17:39 +08:00
var ticks = generateTicks ( span , spanStartTime , spanDuration , reversed , length , tickDistance , minDistanceFromEnd , cancellationToken ) ;
2019-03-08 14:14:57 +08:00
2019-08-01 16:31:26 +08:00
if ( reversed )
{
// For repeat spans, ticks are returned in reverse-StartTime order, which is undesirable for some rulesets
ticks = ticks . Reverse ( ) ;
2019-03-08 14:14:57 +08:00
}
2019-08-01 16:31:26 +08:00
foreach ( var e in ticks )
yield return e ;
2019-03-08 14:14:57 +08:00
if ( span < spanCount - 1 )
{
yield return new SliderEventDescriptor
{
Type = SliderEventType . Repeat ,
SpanIndex = span ,
SpanStartTime = startTime + span * spanDuration ,
2019-03-11 13:36:29 +08:00
Time = spanStartTime + spanDuration ,
2019-03-08 14:14:57 +08:00
PathProgress = ( span + 1 ) % 2 ,
} ;
}
}
}
double totalDuration = spanCount * spanDuration ;
2019-03-08 18:57:30 +08:00
// Okay, I'll level with you. I made a mistake. It was 2007.
// Times were simpler. osu! was but in its infancy and sliders were a new concept.
// A hack was made, which has unfortunately lived through until this day.
//
// This legacy tick is used for some calculations and judgements where audio output is not required.
// Generally we are keeping this around just for difficulty compatibility.
// Optimistically we do not want to ever use this for anything user-facing going forwards.
int finalSpanIndex = spanCount - 1 ;
double finalSpanStartTime = startTime + finalSpanIndex * spanDuration ;
2019-03-11 13:33:21 +08:00
2023-10-04 12:33:06 +08:00
// Note that `finalSpanStartTime + spanDuration ≈ startTime + totalDuration`, but we write it like this to match floating point precision
// of stable.
//
// So thinking about this in a saner way, the time of the LegacyLastTick is
//
// `slider.StartTime + max(slider.Duration / 2, slider.Duration - 36)`
//
// As a slider gets shorter than 72 ms, the leniency offered falls below the 36 ms `TAIL_LENIENCY` constant.
double legacyLastTickTime = Math . Max ( startTime + totalDuration / 2 , ( finalSpanStartTime + spanDuration ) + TAIL_LENIENCY ) ;
double legacyLastTickProgress = ( legacyLastTickTime - finalSpanStartTime ) / spanDuration ;
if ( spanCount % 2 = = 0 ) legacyLastTickProgress = 1 - legacyLastTickProgress ;
2019-03-08 18:57:30 +08:00
yield return new SliderEventDescriptor
{
2023-09-29 15:44:04 +08:00
Type = SliderEventType . LegacyLastTick ,
2019-03-08 18:57:30 +08:00
SpanIndex = finalSpanIndex ,
SpanStartTime = finalSpanStartTime ,
2023-10-04 12:33:06 +08:00
Time = legacyLastTickTime ,
PathProgress = legacyLastTickProgress ,
2019-03-08 18:57:30 +08:00
} ;
yield return new SliderEventDescriptor
2019-03-08 14:14:57 +08:00
{
Type = SliderEventType . Tail ,
2019-03-11 13:33:21 +08:00
SpanIndex = finalSpanIndex ,
2019-03-08 14:14:57 +08:00
SpanStartTime = startTime + ( spanCount - 1 ) * spanDuration ,
2019-03-11 13:36:29 +08:00
Time = startTime + totalDuration ,
2019-03-08 18:57:30 +08:00
PathProgress = spanCount % 2 ,
2019-03-08 14:14:57 +08:00
} ;
}
2019-08-01 16:31:26 +08:00
/// <summary>
/// Generates the ticks for a span of the slider.
/// </summary>
/// <param name="spanIndex">The span index.</param>
/// <param name="spanStartTime">The start time of the span.</param>
/// <param name="spanDuration">The duration of the span.</param>
/// <param name="reversed">Whether the span is reversed.</param>
/// <param name="length">The length of the path.</param>
/// <param name="tickDistance">The distance between each tick.</param>
/// <param name="minDistanceFromEnd">The distance from the end of the path at which ticks are not allowed to be added.</param>
2020-05-15 17:17:39 +08:00
/// <param name="cancellationToken">The cancellation token.</param>
2019-08-01 16:31:26 +08:00
/// <returns>A <see cref="SliderEventDescriptor"/> for each tick. If <paramref name="reversed"/> is true, the ticks will be returned in reverse-StartTime order.</returns>
private static IEnumerable < SliderEventDescriptor > generateTicks ( int spanIndex , double spanStartTime , double spanDuration , bool reversed , double length , double tickDistance ,
2020-05-15 17:17:39 +08:00
double minDistanceFromEnd , CancellationToken cancellationToken = default )
2019-08-01 16:31:26 +08:00
{
2021-10-27 12:04:41 +08:00
for ( double d = tickDistance ; d < = length ; d + = tickDistance )
2019-08-01 16:31:26 +08:00
{
2020-05-15 17:17:39 +08:00
cancellationToken . ThrowIfCancellationRequested ( ) ;
2019-08-01 16:31:26 +08:00
if ( d > = length - minDistanceFromEnd )
break ;
2019-08-01 16:36:20 +08:00
// Always generate ticks from the start of the path rather than the span to ensure that ticks in repeat spans are positioned identically to those in non-repeat spans
2021-10-27 12:04:41 +08:00
double pathProgress = d / length ;
double timeProgress = reversed ? 1 - pathProgress : pathProgress ;
2019-08-01 16:31:26 +08:00
yield return new SliderEventDescriptor
{
Type = SliderEventType . Tick ,
SpanIndex = spanIndex ,
SpanStartTime = spanStartTime ,
Time = spanStartTime + timeProgress * spanDuration ,
PathProgress = pathProgress ,
} ;
}
}
2019-03-08 14:14:57 +08:00
}
2019-03-11 13:36:29 +08:00
/// <summary>
/// Describes a point in time on a slider given special meaning.
/// Should be used by rulesets to visualise the slider.
/// </summary>
2019-03-08 14:14:57 +08:00
public struct SliderEventDescriptor
{
2019-03-11 13:36:29 +08:00
/// <summary>
/// The type of event.
/// </summary>
2019-03-08 14:14:57 +08:00
public SliderEventType Type ;
2019-03-11 13:36:29 +08:00
/// <summary>
/// The time of this event.
/// </summary>
public double Time ;
/// <summary>
2019-03-11 13:53:21 +08:00
/// The zero-based index of the span. In the case of repeat sliders, this will increase after each <see cref="SliderEventType.Repeat"/>.
2019-03-11 13:36:29 +08:00
/// </summary>
2019-03-08 14:14:57 +08:00
public int SpanIndex ;
2019-03-11 13:36:29 +08:00
/// <summary>
/// The time at which the contained <see cref="SpanIndex"/> begins.
/// </summary>
2019-03-08 14:14:57 +08:00
public double SpanStartTime ;
2019-03-11 13:36:29 +08:00
/// <summary>
/// The progress along the slider's <see cref="SliderPath"/> at which this event occurs.
/// </summary>
2019-03-08 14:14:57 +08:00
public double PathProgress ;
}
public enum SliderEventType
{
Tick ,
2023-09-29 13:19:26 +08:00
/// <summary>
2023-09-29 15:44:04 +08:00
/// Occurs just before the tail. See <see cref="SliderEventGenerator.TAIL_LENIENCY"/>.
/// Should generally be ignored.
2023-09-29 13:19:26 +08:00
/// </summary>
2023-09-29 15:44:04 +08:00
LegacyLastTick ,
2019-03-08 14:14:57 +08:00
Head ,
Tail ,
Repeat
}
}