// 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 System.Threading; namespace osu.Game.Rulesets.Objects { public static class SliderEventGenerator { /// /// 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. /// These days, this is implemented in the drawable implementation of Slider in the osu! ruleset. /// /// We need to keep the *only* for osu!catch conversion, which relies on it to generate tiny ticks /// correctly. /// public const double TAIL_LENIENCY = -36; public static IEnumerable Generate(double startTime, double spanDuration, double velocity, double tickDistance, double totalDistance, int spanCount, CancellationToken cancellationToken = default) { // 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; double length = Math.Min(max_length, totalDistance); tickDistance = Math.Clamp(tickDistance, 0, length); double minDistanceFromEnd = velocity * 10; yield return new SliderEventDescriptor { Type = SliderEventType.Head, SpanIndex = 0, SpanStartTime = startTime, Time = startTime, PathProgress = 0, }; if (tickDistance != 0) { for (int span = 0; span < spanCount; span++) { double spanStartTime = startTime + span * spanDuration; bool reversed = span % 2 == 1; var ticks = generateTicks(span, spanStartTime, spanDuration, reversed, length, tickDistance, minDistanceFromEnd, cancellationToken); if (reversed) { // For repeat spans, ticks are returned in reverse-StartTime order, which is undesirable for some rulesets ticks = ticks.Reverse(); } foreach (var e in ticks) yield return e; if (span < spanCount - 1) { yield return new SliderEventDescriptor { Type = SliderEventType.Repeat, SpanIndex = span, SpanStartTime = startTime + span * spanDuration, Time = spanStartTime + spanDuration, PathProgress = (span + 1) % 2, }; } } } double totalDuration = spanCount * spanDuration; // 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; // 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; yield return new SliderEventDescriptor { Type = SliderEventType.LegacyLastTick, SpanIndex = finalSpanIndex, SpanStartTime = finalSpanStartTime, Time = legacyLastTickTime, PathProgress = legacyLastTickProgress, }; yield return new SliderEventDescriptor { Type = SliderEventType.Tail, SpanIndex = finalSpanIndex, SpanStartTime = startTime + (spanCount - 1) * spanDuration, Time = startTime + totalDuration, PathProgress = spanCount % 2, }; } /// /// Generates the ticks for a span of the slider. /// /// The span index. /// The start time of the span. /// The duration of the span. /// Whether the span is reversed. /// The length of the path. /// The distance between each tick. /// The distance from the end of the path at which ticks are not allowed to be added. /// The cancellation token. /// A for each tick. If is true, the ticks will be returned in reverse-StartTime order. private static IEnumerable generateTicks(int spanIndex, double spanStartTime, double spanDuration, bool reversed, double length, double tickDistance, double minDistanceFromEnd, CancellationToken cancellationToken = default) { for (double d = tickDistance; d <= length; d += tickDistance) { cancellationToken.ThrowIfCancellationRequested(); if (d >= length - minDistanceFromEnd) break; // 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 double pathProgress = d / length; double timeProgress = reversed ? 1 - pathProgress : pathProgress; yield return new SliderEventDescriptor { Type = SliderEventType.Tick, SpanIndex = spanIndex, SpanStartTime = spanStartTime, Time = spanStartTime + timeProgress * spanDuration, PathProgress = pathProgress, }; } } } /// /// Describes a point in time on a slider given special meaning. /// Should be used by rulesets to visualise the slider. /// public struct SliderEventDescriptor { /// /// The type of event. /// public SliderEventType Type; /// /// The time of this event. /// public double Time; /// /// The zero-based index of the span. In the case of repeat sliders, this will increase after each . /// public int SpanIndex; /// /// The time at which the contained begins. /// public double SpanStartTime; /// /// The progress along the slider's at which this event occurs. /// public double PathProgress; } public enum SliderEventType { Tick, /// /// Occurs just before the tail. See . /// Should generally be ignored. /// LegacyLastTick, Head, Tail, Repeat } }