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