2019-03-08 19:13:11 +08:00
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
2019-01-24 16:43:03 +08:00
// See the LICENCE file in the repository root for full licence text.
2018-04-13 17:19:50 +08:00
2022-06-17 15:37:17 +08:00
#nullable disable
2018-11-20 15:51:59 +08:00
using osuTK ;
2017-04-18 15:05:58 +08:00
using osu.Game.Rulesets.Objects.Types ;
2017-02-15 17:48:29 +08:00
using System.Collections.Generic ;
2017-04-18 15:05:58 +08:00
using osu.Game.Rulesets.Objects ;
2017-04-06 10:41:16 +08:00
using System.Linq ;
2020-05-15 17:07:41 +08:00
using System.Threading ;
2020-09-14 16:08:22 +08:00
using Newtonsoft.Json ;
2023-04-26 00:12:53 +08:00
using osu.Framework.Bindables ;
2019-01-03 16:43:10 +08:00
using osu.Framework.Caching ;
2017-04-06 10:41:16 +08:00
using osu.Game.Audio ;
2017-07-26 12:22:46 +08:00
using osu.Game.Beatmaps ;
2017-05-23 12:55:18 +08:00
using osu.Game.Beatmaps.ControlPoints ;
2018-08-02 19:36:38 +08:00
using osu.Game.Rulesets.Judgements ;
2023-09-15 18:21:58 +08:00
using osu.Game.Rulesets.Objects.Legacy ;
2020-03-19 17:19:10 +08:00
using osu.Game.Rulesets.Osu.Judgements ;
2019-09-06 14:24:00 +08:00
using osu.Game.Rulesets.Scoring ;
2018-04-13 17:19:50 +08:00
2017-04-18 15:05:58 +08:00
namespace osu.Game.Rulesets.Osu.Objects
2016-09-02 17:35:49 +08:00
{
2023-04-26 19:10:57 +08:00
public class Slider : OsuHitObject , IHasPathWithRepeats , IHasSliderVelocity , IHasGenerateTicks
2016-09-02 17:35:49 +08:00
{
2020-05-27 11:37:44 +08:00
public double EndTime = > StartTime + this . SpanCount ( ) * Path . Distance / Velocity ;
2020-09-14 16:08:22 +08:00
[JsonIgnore]
2020-05-27 11:37:44 +08:00
public double Duration
2020-02-05 16:12:26 +08:00
{
2020-05-27 11:37:44 +08:00
get = > EndTime - StartTime ;
2020-02-06 12:16:32 +08:00
set = > throw new System . NotSupportedException ( $"Adjust via {nameof(RepeatCount)} instead" ) ; // can be implemented if/when needed.
2020-02-05 16:12:26 +08:00
}
2022-03-14 16:17:14 +08:00
public override IList < HitSampleInfo > AuxiliarySamples = > CreateSlidingSamples ( ) . Concat ( TailSamples ) . ToArray ( ) ;
2022-03-14 14:45:57 +08:00
2019-08-09 18:12:29 +08:00
private readonly Cached < Vector2 > endPositionCache = new Cached < Vector2 > ( ) ;
2019-01-03 16:43:10 +08:00
public override Vector2 EndPosition = > endPositionCache . IsValid ? endPositionCache . Value : endPositionCache . Value = Position + this . CurvePositionAt ( 1 ) ;
2018-02-24 01:43:36 +08:00
public Vector2 StackedPositionAt ( double t ) = > StackedPosition + this . CurvePositionAt ( t ) ;
2018-04-13 17:19:50 +08:00
2019-12-09 16:48:27 +08:00
private readonly SliderPath path = new SliderPath ( ) ;
2019-12-06 19:53:40 +08:00
public SliderPath Path
{
get = > path ;
set
{
path . ControlPoints . Clear ( ) ;
path . ExpectedDistance . Value = null ;
if ( value ! = null )
{
2021-08-26 00:42:57 +08:00
path . ControlPoints . AddRange ( value . ControlPoints . Select ( c = > new PathControlPoint ( c . Position , c . Type ) ) ) ;
2019-12-06 19:53:40 +08:00
path . ExpectedDistance . Value = value . ExpectedDistance . Value ;
}
}
}
2018-04-13 17:19:50 +08:00
2018-11-12 13:07:48 +08:00
public double Distance = > Path . Distance ;
2018-04-13 17:19:50 +08:00
2018-10-25 17:16:25 +08:00
public override Vector2 Position
{
get = > base . Position ;
set
{
base . Position = value ;
2019-10-31 14:52:38 +08:00
updateNestedPositions ( ) ;
2018-10-25 17:16:25 +08:00
}
2017-04-21 19:29:27 +08:00
}
2018-04-13 17:19:50 +08:00
2017-11-17 19:28:41 +08:00
/// <summary>
2017-11-17 20:28:59 +08: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 19:28:41 +08:00
/// </summary>
2017-11-17 20:28:59 +08:00
internal Vector2 ? LazyEndPosition ;
2018-04-13 17:19:50 +08:00
2017-11-17 20:28:59 +08: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 17:19:50 +08:00
2021-10-13 23:41:24 +08: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 16:59:07 +08:00
public IList < IList < HitSampleInfo > > NodeSamples { get ; set ; } = new List < IList < HitSampleInfo > > ( ) ;
2018-10-16 16:10:24 +08:00
2021-04-08 19:57:50 +08:00
[JsonIgnore]
2021-04-09 14:28:08 +08:00
public IList < HitSampleInfo > TailSamples { get ; private set ; }
2021-04-08 19:19:41 +08:00
2019-01-03 16:43:10 +08:00
private int repeatCount ;
public int RepeatCount
{
get = > repeatCount ;
set
{
repeatCount = value ;
2020-02-05 16:12:26 +08:00
updateNestedPositions ( ) ;
2019-01-03 16:43:10 +08:00
}
}
2018-04-13 17:19:50 +08:00
2018-01-22 19:36:38 +08:00
/// <summary>
2018-01-24 16:44:50 +08:00
/// The length of one span of this <see cref="Slider"/>.
2018-01-22 19:36:38 +08:00
/// </summary>
2018-01-24 16:44:50 +08:00
public double SpanDuration = > Duration / this . SpanCount ( ) ;
2018-04-13 17:19:50 +08:00
2018-10-15 11:32:59 +08:00
/// <summary>
2023-09-06 17:58:58 +08:00
/// The computed velocity of this <see cref="Slider"/>. This is the amount of path distance travelled in 1 ms.
2018-10-15 11:32:59 +08:00
/// </summary>
2018-10-15 11:31:52 +08:00
public double Velocity { get ; private set ; }
2018-10-15 11:32:59 +08:00
/// <summary>
/// Spacing between <see cref="SliderTick"/>s of this <see cref="Slider"/>.
/// </summary>
2018-10-15 11:31:52 +08:00
public double TickDistance { get ; private set ; }
2018-04-13 17:19:50 +08:00
2018-10-15 11:25:42 +08:00
/// <summary>
2018-10-15 11:32:59 +08:00
/// An extra multiplier that affects the number of <see cref="SliderTick"/>s generated by this <see cref="Slider"/>.
2018-10-15 11:25:42 +08: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 17:19:50 +08:00
2021-02-03 21:12:20 +08:00
/// <summary>
2021-02-10 17:46:26 +08: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 21:12:20 +08:00
/// </summary>
2021-02-10 17:46:26 +08:00
public bool OnlyJudgeNestedObjects = true ;
2021-02-03 21:12:20 +08:00
2023-09-06 17:59:15 +08:00
public BindableNumber < double > SliderVelocityMultiplierBindable { get ; } = new BindableDouble ( 1 )
2023-05-01 01:32:24 +08:00
{
MinValue = 0.1 ,
MaxValue = 10
} ;
2023-04-26 00:12:53 +08:00
2023-09-06 17:59:15 +08:00
public double SliderVelocityMultiplier
2023-04-26 00:12:53 +08:00
{
2023-09-06 17:59:15 +08:00
get = > SliderVelocityMultiplierBindable . Value ;
set = > SliderVelocityMultiplierBindable . Value = value ;
2023-04-26 00:12:53 +08:00
}
2023-04-25 17:34:09 +08:00
2023-04-30 22:03:58 +08:00
public bool GenerateTicks { get ; set ; } = true ;
2023-04-26 19:10:57 +08:00
2020-09-14 16:08:22 +08:00
[JsonIgnore]
2021-02-05 14:56:13 +08:00
public SliderHeadCircle HeadCircle { get ; protected set ; }
2020-09-14 16:08:22 +08:00
[JsonIgnore]
public SliderTailCircle TailCircle { get ; protected set ; }
2018-04-13 17:19:50 +08:00
2019-11-08 14:39:07 +08:00
public Slider ( )
{
2022-06-24 20:25:23 +08:00
SamplesBindable . CollectionChanged + = ( _ , _ ) = > UpdateNestedSamples ( ) ;
2019-12-06 19:53:40 +08:00
Path . Version . ValueChanged + = _ = > updateNestedPositions ( ) ;
2019-11-08 14:39:07 +08:00
}
2021-10-01 13:56:42 +08:00
protected override void ApplyDefaultsToSelf ( ControlPointInfo controlPointInfo , IBeatmapDifficultyInfo difficulty )
2016-11-28 17:45:50 +08:00
{
2017-12-22 20:42:54 +08:00
base . ApplyDefaultsToSelf ( controlPointInfo , difficulty ) ;
2018-04-13 17:19:50 +08:00
2017-05-23 12:55:18 +08:00
TimingControlPoint timingPoint = controlPointInfo . TimingPointAt ( StartTime ) ;
2022-08-24 02:07:18 +08:00
2023-09-15 18:21:58 +08: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 17:19:50 +08:00
2023-04-26 19:10:57 +08:00
TickDistance = GenerateTicks ? ( scoringDistance / difficulty . SliderTickRate * TickDistanceMultiplier ) : double . PositiveInfinity ;
2023-04-25 17:34:09 +08:00
}
2020-05-15 17:07:41 +08:00
protected override void CreateNestedHitObjects ( CancellationToken cancellationToken )
2017-02-13 03:38:05 +08:00
{
2020-05-15 17:07:41 +08:00
base . CreateNestedHitObjects ( cancellationToken ) ;
2018-04-13 17:19:50 +08:00
2023-09-29 13:19:26 +08:00
var sliderEvents = SliderEventGenerator . Generate ( StartTime , SpanDuration , Velocity , TickDistance , Path . Distance , this . SpanCount ( ) , cancellationToken ) ;
2021-10-24 22:51:49 +08:00
foreach ( var e in sliderEvents )
2018-01-30 15:24:23 +08:00
{
2019-03-08 12:48:45 +08:00
switch ( e . Type )
2017-12-22 20:42:54 +08:00
{
2019-03-08 12:48:45 +08:00
case SliderEventType . Tick :
AddNested ( new SliderTick
{
SpanIndex = e . SpanIndex ,
SpanStartTime = e . SpanStartTime ,
2019-03-11 13:36:29 +08:00
StartTime = e . Time ,
2019-03-08 12:48:45 +08:00
Position = Position + Path . PositionAt ( e . PathProgress ) ,
StackHeight = StackHeight ,
Scale = Scale ,
} ) ;
2017-12-22 20:42:54 +08:00
break ;
2019-04-01 11:44:46 +08:00
2019-03-08 12:48:45 +08:00
case SliderEventType . Head :
2020-03-30 15:14:56 +08:00
AddNested ( HeadCircle = new SliderHeadCircle
2018-01-18 18:50:26 +08:00
{
2019-03-11 13:36:29 +08:00
StartTime = e . Time ,
2019-03-08 12:48:45 +08:00
Position = Position ,
2019-10-21 15:15:41 +08:00
StackHeight = StackHeight ,
2018-01-18 18:50:26 +08:00
} ) ;
2019-03-08 12:48:45 +08:00
break ;
2019-04-01 11:44:46 +08:00
2023-09-29 13:19:26 +08:00
case SliderEventType . LastTick :
2023-09-29 13:40:44 +08:00
// Of note, we are directly mapping LastTick (instead of `SliderEventType.Tail`) to SliderTailCircle.
// It is required as difficulty calculation and gameplay relies on reading this value.
// (although it is displayed in classic skins, which may be a concern).
// If this is to change, we should revisit this.
2020-10-02 14:21:52 +08:00
AddNested ( TailCircle = new SliderTailCircle ( this )
2019-03-08 12:48:45 +08:00
{
2020-10-02 13:20:55 +08:00
RepeatIndex = e . SpanIndex ,
2019-03-11 13:36:29 +08:00
StartTime = e . Time ,
2019-03-08 12:48:45 +08:00
Position = EndPosition ,
2019-10-21 15:15:41 +08:00
StackHeight = StackHeight
2019-03-08 12:48:45 +08:00
} ) ;
break ;
2019-04-01 11:44:46 +08:00
2019-03-08 12:48:45 +08:00
case SliderEventType . Repeat :
2020-10-02 14:21:52 +08:00
AddNested ( new SliderRepeat ( this )
2019-03-08 12:48:45 +08:00
{
RepeatIndex = e . SpanIndex ,
StartTime = StartTime + ( e . SpanIndex + 1 ) * SpanDuration ,
2019-03-08 14:14:57 +08:00
Position = Position + Path . PositionAt ( e . PathProgress ) ,
2019-03-08 12:48:45 +08:00
StackHeight = StackHeight ,
Scale = Scale ,
} ) ;
break ;
2017-02-13 03:38:05 +08:00
}
}
2019-11-08 14:39:07 +08:00
2022-03-20 02:29:44 +08:00
UpdateNestedSamples ( ) ;
2017-02-13 03:38:05 +08:00
}
2018-04-13 17:19:50 +08:00
2019-10-31 14:52:38 +08:00
private void updateNestedPositions ( )
{
2019-12-06 19:53:40 +08:00
endPositionCache . Invalidate ( ) ;
2019-10-31 14:52:38 +08:00
if ( HeadCircle ! = null )
HeadCircle . Position = Position ;
if ( TailCircle ! = null )
TailCircle . Position = EndPosition ;
}
2022-03-20 02:29:44 +08:00
protected void UpdateNestedSamples ( )
2019-11-08 14:39:07 +08: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 14:37:51 +08:00
sampleList . Add ( firstSample . With ( "slidertick" ) ) ;
2019-11-08 14:39:07 +08:00
foreach ( var tick in NestedHitObjects . OfType < SliderTick > ( ) )
tick . Samples = sampleList ;
2020-03-19 13:42:02 +08:00
foreach ( var repeat in NestedHitObjects . OfType < SliderRepeat > ( ) )
2020-10-09 19:50:09 +08:00
repeat . Samples = this . GetNodeSamples ( repeat . RepeatIndex + 1 ) ;
2019-11-08 14:39:07 +08:00
if ( HeadCircle ! = null )
2020-10-09 19:50:09 +08:00
HeadCircle . Samples = this . GetNodeSamples ( 0 ) ;
2021-04-09 14:28:08 +08:00
2023-09-29 13:40:44 +08: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 14:28:08 +08:00
// For now, the samples are played by the slider itself at the correct end time.
TailSamples = this . GetNodeSamples ( repeatCount + 1 ) ;
2019-11-08 14:39:07 +08:00
}
2021-02-10 17:46:26 +08:00
public override Judgement CreateJudgement ( ) = > OnlyJudgeNestedObjects ? new OsuIgnoreJudgement ( ) : new OsuJudgement ( ) ;
2019-09-02 15:10:30 +08:00
2019-10-09 18:08:31 +08:00
protected override HitWindows CreateHitWindows ( ) = > HitWindows . Empty ;
2016-09-02 17:35:49 +08:00
}
}