2019-03-08 20:13:11 +09:00
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
2019-01-24 17:43:03 +09:00
// See the LICENCE file in the repository root for full licence text.
2018-04-13 18:19:50 +09:00
2022-06-17 16:37:17 +09:00
#nullable disable
2018-11-20 16:51:59 +09:00
using osuTK ;
2017-04-18 16:05:58 +09:00
using osu.Game.Rulesets.Objects.Types ;
2017-02-15 10:48:29 +01:00
using System.Collections.Generic ;
2017-04-18 16:05:58 +09:00
using osu.Game.Rulesets.Objects ;
2017-04-06 11:41:16 +09:00
using System.Linq ;
2020-05-15 18:07:41 +09:00
using System.Threading ;
2020-09-14 17:08:22 +09:00
using Newtonsoft.Json ;
2023-04-25 18:12:53 +02:00
using osu.Framework.Bindables ;
2019-01-03 17:43:10 +09:00
using osu.Framework.Caching ;
2017-04-06 11:41:16 +09:00
using osu.Game.Audio ;
2017-07-26 13:22:46 +09:00
using osu.Game.Beatmaps ;
2017-05-23 13:55:18 +09:00
using osu.Game.Beatmaps.ControlPoints ;
2018-08-02 20:36:38 +09:00
using osu.Game.Rulesets.Judgements ;
2023-09-15 12:21:58 +02:00
using osu.Game.Rulesets.Objects.Legacy ;
2020-03-19 18:19:10 +09:00
using osu.Game.Rulesets.Osu.Judgements ;
2019-09-06 15:24:00 +09:00
using osu.Game.Rulesets.Scoring ;
2018-04-13 18:19:50 +09:00
2017-04-18 16:05:58 +09:00
namespace osu.Game.Rulesets.Osu.Objects
2016-09-02 18:35:49 +09:00
{
2023-04-26 13:10:57 +02:00
public class Slider : OsuHitObject , IHasPathWithRepeats , IHasSliderVelocity , IHasGenerateTicks
2016-09-02 18:35:49 +09:00
{
2020-05-27 12:37:44 +09:00
public double EndTime = > StartTime + this . SpanCount ( ) * Path . Distance / Velocity ;
2020-09-14 17:08:22 +09:00
[JsonIgnore]
2020-05-27 12:37:44 +09:00
public double Duration
2020-02-05 17:12:26 +09:00
{
2020-05-27 12:37:44 +09:00
get = > EndTime - StartTime ;
2020-02-06 13:16:32 +09:00
set = > throw new System . NotSupportedException ( $"Adjust via {nameof(RepeatCount)} instead" ) ; // can be implemented if/when needed.
2020-02-05 17:12:26 +09:00
}
2022-03-14 17:17:14 +09:00
public override IList < HitSampleInfo > AuxiliarySamples = > CreateSlidingSamples ( ) . Concat ( TailSamples ) . ToArray ( ) ;
2022-03-14 15:45:57 +09:00
2019-08-09 19:12:29 +09:00
private readonly Cached < Vector2 > endPositionCache = new Cached < Vector2 > ( ) ;
2019-01-03 17:43:10 +09:00
public override Vector2 EndPosition = > endPositionCache . IsValid ? endPositionCache . Value : endPositionCache . Value = Position + this . CurvePositionAt ( 1 ) ;
2018-02-24 02:43:36 +09:00
public Vector2 StackedPositionAt ( double t ) = > StackedPosition + this . CurvePositionAt ( t ) ;
2018-04-13 18:19:50 +09:00
2019-12-09 17:48:27 +09:00
private readonly SliderPath path = new SliderPath ( ) ;
2019-12-06 20:53:40 +09:00
public SliderPath Path
{
get = > path ;
set
{
path . ControlPoints . Clear ( ) ;
2023-10-30 15:10:10 +09:00
path . ControlPoints . AddRange ( value . ControlPoints . Select ( c = > new PathControlPoint ( c . Position , c . Type ) ) ) ;
2019-12-06 20:53:40 +09:00
2023-10-30 15:10:10 +09:00
path . ExpectedDistance . Value = value . ExpectedDistance . Value ;
2019-12-06 20:53:40 +09:00
}
}
2018-04-13 18:19:50 +09:00
2018-11-12 14:07:48 +09:00
public double Distance = > Path . Distance ;
2018-04-13 18:19:50 +09:00
2018-10-25 18:16:25 +09:00
public override Vector2 Position
{
get = > base . Position ;
set
{
base . Position = value ;
2019-10-31 15:52:38 +09:00
updateNestedPositions ( ) ;
2018-10-25 18:16:25 +09:00
}
2017-04-21 20:29:27 +09:00
}
2018-04-13 18:19:50 +09:00
2017-11-17 20:28:41 +09:00
/// <summary>
2017-11-17 21:28:59 +09:00
/// The position of the cursor at the point of completion of this <see cref="Slider"/> if it was hit
/// with as few movements as possible. This is set and used by difficulty calculation.
2017-11-17 20:28:41 +09:00
/// </summary>
2017-11-17 21:28:59 +09:00
internal Vector2 ? LazyEndPosition ;
2018-04-13 18:19:50 +09:00
2017-11-17 21:28:59 +09:00
/// <summary>
/// The distance travelled by the cursor upon completion of this <see cref="Slider"/> if it was hit
/// with as few movements as possible. This is set and used by difficulty calculation.
/// </summary>
internal float LazyTravelDistance ;
2018-04-13 18:19:50 +09:00
2021-10-13 15:41:24 +00:00
/// <summary>
/// The time taken by the cursor upon completion of this <see cref="Slider"/> if it was hit
/// with as few movements as possible. This is set and used by difficulty calculation.
/// </summary>
internal double LazyTravelTime ;
2021-10-23 01:59:07 -07:00
public IList < IList < HitSampleInfo > > NodeSamples { get ; set ; } = new List < IList < HitSampleInfo > > ( ) ;
2018-10-16 17:10:24 +09:00
2021-04-08 20:57:50 +09:00
[JsonIgnore]
2021-04-09 15:28:08 +09:00
public IList < HitSampleInfo > TailSamples { get ; private set ; }
2021-04-08 20:19:41 +09:00
2019-01-03 17:43:10 +09:00
private int repeatCount ;
public int RepeatCount
{
get = > repeatCount ;
set
{
repeatCount = value ;
2024-01-31 12:43:24 +01:00
updateNestedPositions ( ) ;
2019-01-03 17:43:10 +09:00
}
}
2018-04-13 18:19:50 +09:00
2018-01-22 12:36:38 +01:00
/// <summary>
2018-01-24 09:44:50 +01:00
/// The length of one span of this <see cref="Slider"/>.
2018-01-22 12:36:38 +01:00
/// </summary>
2018-01-24 09:44:50 +01:00
public double SpanDuration = > Duration / this . SpanCount ( ) ;
2018-04-13 18:19:50 +09:00
2018-10-15 12:32:59 +09:00
/// <summary>
2023-09-06 18:58:58 +09:00
/// The computed velocity of this <see cref="Slider"/>. This is the amount of path distance travelled in 1 ms.
2018-10-15 12:32:59 +09:00
/// </summary>
2018-10-15 12:31:52 +09:00
public double Velocity { get ; private set ; }
2018-10-15 12:32:59 +09:00
/// <summary>
/// Spacing between <see cref="SliderTick"/>s of this <see cref="Slider"/>.
/// </summary>
2018-10-15 12:31:52 +09:00
public double TickDistance { get ; private set ; }
2018-04-13 18:19:50 +09:00
2018-10-15 12:25:42 +09:00
/// <summary>
2018-10-15 12:32:59 +09:00
/// An extra multiplier that affects the number of <see cref="SliderTick"/>s generated by this <see cref="Slider"/>.
2018-10-15 12:25:42 +09:00
/// An increase in this value increases <see cref="TickDistance"/>, which reduces the number of ticks generated.
/// </summary>
public double TickDistanceMultiplier = 1 ;
2018-04-13 18:19:50 +09:00
2021-02-03 22:12:20 +09:00
/// <summary>
2023-11-02 18:58:14 +01:00
/// If <see langword="false"/>, <see cref="Slider"/>'s judgement is fully handled by its nested <see cref="HitObject"/>s.
/// If <see langword="true"/>, this <see cref="Slider"/> will be judged proportionally to the number of nested <see cref="HitObject"/>s hit.
2021-02-03 22:12:20 +09:00
/// </summary>
2023-11-02 17:39:23 +09:00
public bool ClassicSliderBehaviour
{
get = > classicSliderBehaviour ;
set
{
classicSliderBehaviour = value ;
if ( HeadCircle ! = null )
HeadCircle . ClassicSliderBehaviour = value ;
2023-11-09 21:48:41 +09:00
if ( TailCircle ! = null )
TailCircle . ClassicSliderBehaviour = value ;
2023-11-02 17:39:23 +09:00
}
}
private bool classicSliderBehaviour ;
2021-02-03 22:12:20 +09:00
2023-09-06 18:59:15 +09:00
public BindableNumber < double > SliderVelocityMultiplierBindable { get ; } = new BindableDouble ( 1 )
2023-04-30 19:32:24 +02:00
{
MinValue = 0.1 ,
MaxValue = 10
} ;
2023-04-25 18:12:53 +02:00
2023-09-06 18:59:15 +09:00
public double SliderVelocityMultiplier
2023-04-25 18:12:53 +02:00
{
2023-09-06 18:59:15 +09:00
get = > SliderVelocityMultiplierBindable . Value ;
set = > SliderVelocityMultiplierBindable . Value = value ;
2023-04-25 18:12:53 +02:00
}
2023-04-25 11:34:09 +02:00
2023-04-30 16:03:58 +02:00
public bool GenerateTicks { get ; set ; } = true ;
2023-04-26 13:10:57 +02:00
2020-09-14 17:08:22 +09:00
[JsonIgnore]
2021-02-05 15:56:13 +09:00
public SliderHeadCircle HeadCircle { get ; protected set ; }
2020-09-14 17:08:22 +09:00
[JsonIgnore]
public SliderTailCircle TailCircle { get ; protected set ; }
2018-04-13 18:19:50 +09:00
2019-11-08 15:39:07 +09:00
public Slider ( )
{
2022-06-24 21:25:23 +09:00
SamplesBindable . CollectionChanged + = ( _ , _ ) = > UpdateNestedSamples ( ) ;
2024-01-31 12:43:24 +01:00
Path . Version . ValueChanged + = _ = > updateNestedPositions ( ) ;
2019-11-08 15:39:07 +09:00
}
2021-10-01 14:56:42 +09:00
protected override void ApplyDefaultsToSelf ( ControlPointInfo controlPointInfo , IBeatmapDifficultyInfo difficulty )
2016-11-28 18:45:50 +09:00
{
2017-12-22 21:42:54 +09:00
base . ApplyDefaultsToSelf ( controlPointInfo , difficulty ) ;
2018-04-13 18:19:50 +09:00
2017-05-23 13:55:18 +09:00
TimingControlPoint timingPoint = controlPointInfo . TimingPointAt ( StartTime ) ;
2022-08-23 14:07:18 -04:00
2023-09-15 12:21:58 +02:00
Velocity = BASE_SCORING_DISTANCE * difficulty . SliderMultiplier / LegacyRulesetExtensions . GetPrecisionAdjustedBeatLength ( this , timingPoint , OsuRuleset . SHORT_NAME ) ;
// WARNING: this is intentionally not computed as `BASE_SCORING_DISTANCE * difficulty.SliderMultiplier`
// for backwards compatibility reasons (intentionally introducing floating point errors to match stable).
double scoringDistance = Velocity * timingPoint . BeatLength ;
2018-04-13 18:19:50 +09:00
2023-04-26 13:10:57 +02:00
TickDistance = GenerateTicks ? ( scoringDistance / difficulty . SliderTickRate * TickDistanceMultiplier ) : double . PositiveInfinity ;
2023-04-25 11:34:09 +02:00
}
2020-05-15 18:07:41 +09:00
protected override void CreateNestedHitObjects ( CancellationToken cancellationToken )
2017-02-12 20:38:05 +01:00
{
2020-05-15 18:07:41 +09:00
base . CreateNestedHitObjects ( cancellationToken ) ;
2018-04-13 18:19:50 +09:00
2023-09-29 14:19:26 +09:00
var sliderEvents = SliderEventGenerator . Generate ( StartTime , SpanDuration , Velocity , TickDistance , Path . Distance , this . SpanCount ( ) , cancellationToken ) ;
2021-10-24 23:51:49 +09:00
foreach ( var e in sliderEvents )
2018-01-30 16:24:23 +09:00
{
2019-03-08 13:48:45 +09:00
switch ( e . Type )
2017-12-22 21:42:54 +09:00
{
2019-03-08 13:48:45 +09:00
case SliderEventType . Tick :
AddNested ( new SliderTick
{
SpanIndex = e . SpanIndex ,
SpanStartTime = e . SpanStartTime ,
2019-03-11 14:36:29 +09:00
StartTime = e . Time ,
2019-03-08 13:48:45 +09:00
Position = Position + Path . PositionAt ( e . PathProgress ) ,
StackHeight = StackHeight ,
} ) ;
2017-12-22 21:42:54 +09:00
break ;
2019-04-01 12:44:46 +09:00
2019-03-08 13:48:45 +09:00
case SliderEventType . Head :
2020-03-30 16:14:56 +09:00
AddNested ( HeadCircle = new SliderHeadCircle
2018-01-18 19:50:26 +09:00
{
2019-03-11 14:36:29 +09:00
StartTime = e . Time ,
2019-03-08 13:48:45 +09:00
Position = Position ,
2019-10-21 16:15:41 +09:00
StackHeight = StackHeight ,
2023-11-02 17:39:23 +09:00
ClassicSliderBehaviour = ClassicSliderBehaviour ,
2018-01-18 19:50:26 +09:00
} ) ;
2019-03-08 13:48:45 +09:00
break ;
2019-04-01 12:44:46 +09:00
2023-09-29 16:20:21 +09:00
case SliderEventType . Tail :
2020-10-02 15:21:52 +09:00
AddNested ( TailCircle = new SliderTailCircle ( this )
2019-03-08 13:48:45 +09:00
{
2020-10-02 14:20:55 +09:00
RepeatIndex = e . SpanIndex ,
2019-03-11 14:36:29 +09:00
StartTime = e . Time ,
2024-01-13 13:54:04 +01:00
Position = EndPosition ,
2023-11-02 17:39:41 +09:00
StackHeight = StackHeight ,
2023-11-09 21:48:41 +09:00
ClassicSliderBehaviour = ClassicSliderBehaviour ,
2019-03-08 13:48:45 +09:00
} ) ;
break ;
2019-04-01 12:44:46 +09:00
2019-03-08 13:48:45 +09:00
case SliderEventType . Repeat :
2020-10-02 15:21:52 +09:00
AddNested ( new SliderRepeat ( this )
2019-03-08 13:48:45 +09:00
{
RepeatIndex = e . SpanIndex ,
StartTime = StartTime + ( e . SpanIndex + 1 ) * SpanDuration ,
2019-03-08 15:14:57 +09:00
Position = Position + Path . PositionAt ( e . PathProgress ) ,
2019-03-08 13:48:45 +09:00
StackHeight = StackHeight ,
} ) ;
break ;
2017-02-12 20:38:05 +01:00
}
}
2019-11-08 15:39:07 +09:00
2022-03-19 18:29:44 +00:00
UpdateNestedSamples ( ) ;
2017-02-12 20:38:05 +01:00
}
2018-04-13 18:19:50 +09:00
2019-10-31 15:52:38 +09:00
private void updateNestedPositions ( )
{
2019-12-06 20:53:40 +09:00
endPositionCache . Invalidate ( ) ;
2019-10-31 15:52:38 +09:00
if ( HeadCircle ! = null )
HeadCircle . Position = Position ;
2024-01-13 13:54:04 +01:00
if ( TailCircle ! = null )
TailCircle . Position = EndPosition ;
2019-10-31 15:52:38 +09:00
}
2022-03-19 18:29:44 +00:00
protected void UpdateNestedSamples ( )
2019-11-08 15:39:07 +09:00
{
2024-02-18 03:22:10 +08:00
// TODO: remove this when guaranteed sort is present for samples (https://github.com/ppy/osu/issues/1933)
2024-02-17 23:28:35 +03:00
HitSampleInfo tickSample = ( Samples . FirstOrDefault ( s = > s . Name = = HitSampleInfo . HIT_NORMAL ) ? ? Samples . FirstOrDefault ( ) ) ? . With ( "slidertick" ) ;
2019-11-08 15:39:07 +09:00
2024-02-17 15:46:38 +03:00
foreach ( var nested in NestedHitObjects )
{
switch ( nested )
{
case SliderTick tick :
2024-02-18 03:22:10 +08:00
tick . SamplesBindable . Clear ( ) ;
2024-02-17 23:28:35 +03:00
if ( tickSample ! = null )
tick . SamplesBindable . Add ( tickSample ) ;
2024-02-17 15:46:38 +03:00
break ;
2019-11-08 15:39:07 +09:00
2024-02-17 15:46:38 +03:00
case SliderRepeat repeat :
repeat . Samples = this . GetNodeSamples ( repeat . RepeatIndex + 1 ) ;
break ;
}
}
2019-11-08 15:39:07 +09:00
if ( HeadCircle ! = null )
2020-10-09 20:50:09 +09:00
HeadCircle . Samples = this . GetNodeSamples ( 0 ) ;
2021-04-09 15:28:08 +09:00
2023-09-29 14:40:44 +09:00
// The samples should be attached to the slider tail, however this can only be done if LastTick is removed otherwise they would play earlier than they're intended to.
// (see mapping logic in `CreateNestedHitObjects` above)
//
2021-04-09 15:28:08 +09:00
// For now, the samples are played by the slider itself at the correct end time.
TailSamples = this . GetNodeSamples ( repeatCount + 1 ) ;
2019-11-08 15:39:07 +09:00
}
2023-11-02 17:04:49 +09:00
public override Judgement CreateJudgement ( ) = > ClassicSliderBehaviour
2023-11-10 14:00:34 +09:00
// Final combo is provided by the slider itself - see logic in `DrawableSlider.CheckForResult()`
2023-11-02 17:04:49 +09:00
? new OsuJudgement ( )
2023-11-10 14:00:34 +09:00
// Final combo is provided by the tail circle - see `SliderTailCircle`
2023-11-02 17:04:49 +09:00
: new OsuIgnoreJudgement ( ) ;
2019-09-02 16:10:30 +09:00
2019-10-09 19:08:31 +09:00
protected override HitWindows CreateHitWindows ( ) = > HitWindows . Empty ;
2016-09-02 18:35:49 +09:00
}
}