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 ;
2018-04-13 17:19:50 +08:00
using osu.Game.Rulesets.Objects.Types ;
using System.Collections.Generic ;
using osu.Game.Rulesets.Objects ;
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 ;
2019-01-03 16:43:10 +08:00
using osu.Framework.Caching ;
2018-04-13 17:19:50 +08:00
using osu.Game.Audio ;
using osu.Game.Beatmaps ;
using osu.Game.Beatmaps.ControlPoints ;
2022-08-24 02:07:18 +08:00
using osu.Game.Beatmaps.Formats ;
2018-08-02 19:36:38 +08:00
using osu.Game.Rulesets.Judgements ;
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
namespace osu.Game.Rulesets.Osu.Objects
{
2020-05-26 16:44:47 +08:00
public class Slider : OsuHitObject , IHasPathWithRepeats
2018-04-13 17:19:50 +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
public IList < HitSampleInfo > CreateSlidingSamples ( )
{
var slidingSamples = new List < HitSampleInfo > ( ) ;
var normalSample = Samples . FirstOrDefault ( s = > s . Name = = HitSampleInfo . HIT_NORMAL ) ;
if ( normalSample ! = null )
slidingSamples . Add ( normalSample . With ( "sliderslide" ) ) ;
var whistleSample = Samples . FirstOrDefault ( s = > s . Name = = HitSampleInfo . HIT_WHISTLE ) ;
if ( whistleSample ! = null )
slidingSamples . Add ( whistleSample . With ( "sliderwhistle" ) ) ;
return slidingSamples ;
}
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-04-13 17:19:50 +08:00
public Vector2 StackedPositionAt ( double t ) = > StackedPosition + this . CurvePositionAt ( t ) ;
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
}
2018-04-13 17:19:50 +08:00
}
2018-06-13 21:20:34 +08:00
public double? LegacyLastTickOffset { get ; set ; }
2018-04-13 17:19:50 +08:00
/// <summary>
/// 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.
/// </summary>
internal Vector2 ? LazyEndPosition ;
/// <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 ;
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
/// <summary>
/// The length of one span of this <see cref="Slider"/>.
/// </summary>
public double SpanDuration = > Duration / this . SpanCount ( ) ;
2018-10-15 11:32:59 +08:00
/// <summary>
/// Velocity of this <see cref="Slider"/>.
/// </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
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 )
2018-04-13 17:19:50 +08:00
{
base . ApplyDefaultsToSelf ( controlPointInfo , difficulty ) ;
TimingControlPoint timingPoint = controlPointInfo . TimingPointAt ( StartTime ) ;
2022-08-24 02:07:18 +08:00
#pragma warning disable 618
2022-08-24 02:18:40 +08:00
var legacyDifficultyPoint = DifficultyControlPoint as LegacyBeatmapDecoder . LegacyDifficultyControlPoint ;
2022-08-24 02:07:18 +08:00
#pragma warning restore 618
2021-08-31 22:59:36 +08:00
double scoringDistance = BASE_SCORING_DISTANCE * difficulty . SliderMultiplier * DifficultyControlPoint . SliderVelocity ;
2022-08-24 02:07:18 +08:00
bool generateTicks = legacyDifficultyPoint ? . GenerateTicks ? ? true ;
2018-04-13 17:19:50 +08:00
Velocity = scoringDistance / timingPoint . BeatLength ;
2022-08-24 02:07:18 +08:00
TickDistance = generateTicks ? ( scoringDistance / difficulty . SliderTickRate * TickDistanceMultiplier ) : double . PositiveInfinity ;
2018-04-13 17:19:50 +08:00
}
2020-05-15 17:07:41 +08:00
protected override void CreateNestedHitObjects ( CancellationToken cancellationToken )
2018-04-13 17:19:50 +08:00
{
2020-05-15 17:07:41 +08:00
base . CreateNestedHitObjects ( cancellationToken ) ;
2018-04-13 17:19:50 +08:00
2022-08-23 11:31:24 +08:00
var sliderEvents = SliderEventGenerator . Generate ( StartTime , SpanDuration , Velocity , TickDistance , Path . Distance , this . SpanCount ( ) , LegacyLastTickOffset , cancellationToken ) ;
2021-10-24 22:51:49 +08:00
foreach ( var e in sliderEvents )
2018-04-13 17:19:50 +08:00
{
2019-03-08 12:48:45 +08:00
switch ( e . Type )
2018-04-13 17:19:50 +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 ,
} ) ;
2018-04-13 17:19:50 +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-04-13 17:19:50 +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-04-13 17:19:50 +08:00
} ) ;
2019-03-08 12:48:45 +08:00
break ;
2019-04-01 11:44:46 +08:00
2019-03-08 19:13:11 +08:00
case SliderEventType . LegacyLastTick :
2019-03-08 19:12:48 +08: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 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 ;
2018-04-13 17:19:50 +08:00
}
}
2019-11-08 14:39:07 +08:00
2022-03-20 02:29:44 +08:00
UpdateNestedSamples ( ) ;
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
// 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 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 ;
2018-04-13 17:19:50 +08:00
}
}