2019-01-24 16:43:03 +08:00
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// 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
2017-11-08 06:08:24 +08:00
using System ;
2017-05-23 12:55:18 +08:00
using System.Collections.Generic ;
using System.Linq ;
2021-06-16 15:24:30 +08:00
using JetBrains.Annotations ;
2017-12-05 23:37:37 +08:00
using Newtonsoft.Json ;
2019-10-28 10:39:53 +08:00
using osu.Framework.Bindables ;
2017-12-21 18:40:41 +08:00
using osu.Framework.Lists ;
2021-05-01 14:13:42 +08:00
using osu.Framework.Utils ;
2021-04-28 15:47:30 +08:00
using osu.Game.Screens.Edit ;
2021-07-19 11:38:22 +08:00
using osu.Game.Utils ;
2018-04-13 17:19:50 +08:00
2017-05-23 12:55:18 +08:00
namespace osu.Game.Beatmaps.ControlPoints
{
2017-12-06 15:23:51 +08:00
[Serializable]
2024-06-17 16:13:44 +08:00
[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)]
2021-07-19 11:38:22 +08:00
public class ControlPointInfo : IDeepCloneable < ControlPointInfo >
2017-05-23 12:55:18 +08:00
{
2024-06-11 15:15:14 +08:00
/// <summary>
/// Invoked on any change to the set of control points.
/// </summary>
[CanBeNull]
public event Action ControlPointsChanged ;
private void raiseControlPointsChanged ( [ CanBeNull ] ControlPoint _ = null ) = > ControlPointsChanged ? . Invoke ( ) ;
2019-10-25 18:48:01 +08:00
/// <summary>
2019-10-28 10:40:33 +08:00
/// All control points grouped by time.
2019-10-25 18:48:01 +08:00
/// </summary>
[JsonProperty]
2019-10-28 10:39:53 +08:00
public IBindableList < ControlPointGroup > Groups = > groups ;
2019-10-25 18:48:01 +08:00
2019-10-28 10:39:53 +08:00
private readonly BindableList < ControlPointGroup > groups = new BindableList < ControlPointGroup > ( ) ;
2019-10-25 18:48:01 +08:00
2017-05-23 14:29:38 +08:00
/// <summary>
/// All timing points.
/// </summary>
2017-12-06 15:23:51 +08:00
[JsonProperty]
2019-10-25 18:48:01 +08:00
public IReadOnlyList < TimingControlPoint > TimingPoints = > timingPoints ;
private readonly SortedList < TimingControlPoint > timingPoints = new SortedList < TimingControlPoint > ( Comparer < TimingControlPoint > . Default ) ;
2018-04-13 17:19:50 +08:00
2017-05-23 14:29:38 +08:00
/// <summary>
/// All effect points.
/// </summary>
2017-12-21 18:40:41 +08:00
[JsonProperty]
2019-10-25 18:48:01 +08:00
public IReadOnlyList < EffectControlPoint > EffectPoints = > effectPoints ;
private readonly SortedList < EffectControlPoint > effectPoints = new SortedList < EffectControlPoint > ( Comparer < EffectControlPoint > . Default ) ;
/// <summary>
/// All control points, of all types.
/// </summary>
2020-04-21 12:58:23 +08:00
[JsonIgnore]
2019-10-25 18:48:01 +08:00
public IEnumerable < ControlPoint > AllControlPoints = > Groups . SelectMany ( g = > g . ControlPoints ) . ToArray ( ) ;
2018-04-13 17:19:50 +08:00
2017-05-23 12:55:18 +08:00
/// <summary>
/// Finds the effect control point that is active at <paramref name="time"/>.
/// </summary>
/// <param name="time">The time to find the effect control point at.</param>
/// <returns>The effect control point.</returns>
2021-06-16 15:24:30 +08:00
[NotNull]
2021-08-25 17:00:57 +08:00
public EffectControlPoint EffectPointAt ( double time ) = > BinarySearchWithFallback ( EffectPoints , time , EffectControlPoint . DEFAULT ) ;
2018-04-13 17:19:50 +08:00
2017-05-23 12:55:18 +08:00
/// <summary>
/// Finds the timing control point that is active at <paramref name="time"/>.
/// </summary>
/// <param name="time">The time to find the timing control point at.</param>
/// <returns>The timing control point.</returns>
2021-06-16 15:24:30 +08:00
[NotNull]
2021-08-25 17:00:57 +08:00
public TimingControlPoint TimingPointAt ( double time ) = > BinarySearchWithFallback ( TimingPoints , time , TimingPoints . Count > 0 ? TimingPoints [ 0 ] : TimingControlPoint . DEFAULT ) ;
2018-04-13 17:19:50 +08:00
2024-08-16 07:01:24 +08:00
/// <summary>
/// Finds the first timing point that is active strictly after <paramref name="time"/>, or null if no such point exists.
/// </summary>
/// <param name="time">The time after which to find the timing control point.</param>
/// <returns>The timing control point.</returns>
[CanBeNull]
public TimingControlPoint TimingPointAfter ( double time )
{
int index = BinarySearchUtils . BinarySearch ( TimingPoints , time , c = > c . Time , EqualitySelection . Rightmost ) ;
index = index < 0 ? ~ index : index + 1 ;
return index < TimingPoints . Count ? TimingPoints [ index ] : null ;
}
2017-05-23 12:55:18 +08:00
/// <summary>
/// Finds the maximum BPM represented by any timing control point.
/// </summary>
2018-01-05 00:20:24 +08:00
[JsonIgnore]
2017-05-23 12:55:18 +08:00
public double BPMMaximum = >
2022-12-19 15:42:21 +08:00
60000 / ( TimingPoints . MinBy ( c = > c . BeatLength ) ? ? TimingControlPoint . DEFAULT ) . BeatLength ;
2018-04-13 17:19:50 +08:00
2017-05-23 12:55:18 +08:00
/// <summary>
/// Finds the minimum BPM represented by any timing control point.
/// </summary>
2018-01-05 00:20:24 +08:00
[JsonIgnore]
2017-05-23 12:55:18 +08:00
public double BPMMinimum = >
2022-12-19 15:42:21 +08:00
60000 / ( TimingPoints . MaxBy ( c = > c . BeatLength ) ? ? TimingControlPoint . DEFAULT ) . BeatLength ;
2018-04-13 17:19:50 +08:00
2019-10-28 10:40:33 +08:00
/// <summary>
/// Remove all <see cref="ControlPointGroup"/>s and return to a pristine state.
/// </summary>
2021-08-25 17:00:57 +08:00
public virtual void Clear ( )
2019-10-28 10:40:33 +08:00
{
groups . Clear ( ) ;
timingPoints . Clear ( ) ;
effectPoints . Clear ( ) ;
}
/// <summary>
/// Add a new <see cref="ControlPoint"/>. Note that the provided control point may not be added if the correct state is already present at the provided time.
/// </summary>
/// <param name="time">The time at which the control point should be added.</param>
/// <param name="controlPoint">The control point to add.</param>
/// <returns>Whether the control point was added.</returns>
public bool Add ( double time , ControlPoint controlPoint )
{
2021-08-25 17:00:57 +08:00
if ( CheckAlreadyExisting ( time , controlPoint ) )
2019-10-28 10:40:33 +08:00
return false ;
GroupAt ( time , true ) . Add ( controlPoint ) ;
return true ;
}
public ControlPointGroup GroupAt ( double time , bool addIfNotExisting = false )
{
var newGroup = new ControlPointGroup ( time ) ;
int i = groups . BinarySearch ( newGroup ) ;
if ( i > = 0 )
return groups [ i ] ;
if ( addIfNotExisting )
{
2021-08-25 17:00:57 +08:00
newGroup . ItemAdded + = GroupItemAdded ;
2024-06-11 15:15:14 +08:00
newGroup . ItemChanged + = raiseControlPointsChanged ;
2021-08-25 17:00:57 +08:00
newGroup . ItemRemoved + = GroupItemRemoved ;
2019-10-28 10:40:33 +08:00
groups . Insert ( ~ i , newGroup ) ;
return newGroup ;
}
return null ;
}
public void RemoveGroup ( ControlPointGroup group )
{
2020-10-02 14:25:35 +08:00
foreach ( var item in group . ControlPoints . ToArray ( ) )
group . Remove ( item ) ;
2021-08-25 17:00:57 +08:00
group . ItemAdded - = GroupItemAdded ;
2024-06-11 15:15:14 +08:00
group . ItemChanged - = raiseControlPointsChanged ;
2021-08-25 17:00:57 +08:00
group . ItemRemoved - = GroupItemRemoved ;
2019-10-28 10:40:33 +08:00
groups . Remove ( group ) ;
}
2021-04-28 15:47:30 +08:00
/// <summary>
/// Returns the time on the given beat divisor closest to the given time.
/// </summary>
/// <param name="time">The time to find the closest snapped time to.</param>
/// <param name="beatDivisor">The beat divisor to snap to.</param>
/// <param name="referenceTime">An optional reference point to use for timing point lookup.</param>
2021-04-28 15:59:49 +08:00
public double GetClosestSnappedTime ( double time , int beatDivisor , double? referenceTime = null )
2021-04-28 15:47:30 +08:00
{
var timingPoint = TimingPointAt ( referenceTime ? ? time ) ;
2024-08-16 07:01:24 +08:00
double snappedTime = getClosestSnappedTime ( timingPoint , time , beatDivisor ) ;
if ( referenceTime . HasValue )
return snappedTime ;
// If there is a timing point right after the given time, we should check if it is closer than the snapped time and snap to it.
var timingPointAfter = TimingPointAfter ( time ) ;
return timingPointAfter is null | | Math . Abs ( time - snappedTime ) < Math . Abs ( time - timingPointAfter . Time ) ? snappedTime : timingPointAfter . Time ;
2021-04-28 15:47:30 +08:00
}
/// <summary>
2021-04-28 16:16:05 +08:00
/// Returns the time on *ANY* valid beat divisor, favouring the divisor closest to the given time.
2021-04-28 15:47:30 +08:00
/// </summary>
/// <param name="time">The time to find the closest snapped time to.</param>
2021-04-28 16:16:05 +08:00
public double GetClosestSnappedTime ( double time ) = > GetClosestSnappedTime ( time , GetClosestBeatDivisor ( time ) ) ;
2021-04-28 15:47:30 +08:00
/// <summary>
2021-04-28 16:16:05 +08:00
/// Returns the beat snap divisor closest to the given time. If two are equally close, the smallest divisor is returned.
2021-04-28 15:47:30 +08:00
/// </summary>
/// <param name="time">The time to find the closest beat snap divisor to.</param>
/// <param name="referenceTime">An optional reference point to use for timing point lookup.</param>
2021-04-28 15:57:52 +08:00
public int GetClosestBeatDivisor ( double time , double? referenceTime = null )
2021-04-28 15:47:30 +08:00
{
2021-04-28 16:16:05 +08:00
TimingControlPoint timingPoint = TimingPointAt ( referenceTime ? ? time ) ;
int closestDivisor = 0 ;
double closestTime = double . MaxValue ;
2022-02-13 22:50:40 +08:00
foreach ( int divisor in BindableBeatDivisor . PREDEFINED_DIVISORS )
2021-04-28 16:16:05 +08:00
{
double distanceFromSnap = Math . Abs ( time - getClosestSnappedTime ( timingPoint , time , divisor ) ) ;
2021-04-28 15:47:30 +08:00
2021-05-01 14:13:42 +08:00
if ( Precision . DefinitelyBigger ( closestTime , distanceFromSnap ) )
2021-04-28 16:16:05 +08:00
{
closestDivisor = divisor ;
closestTime = distanceFromSnap ;
}
}
2021-04-28 15:47:30 +08:00
2021-04-28 16:16:05 +08:00
return closestDivisor ;
}
private static double getClosestSnappedTime ( TimingControlPoint timingPoint , double time , int beatDivisor )
{
2021-10-27 12:04:41 +08:00
double beatLength = timingPoint . BeatLength / beatDivisor ;
2023-01-12 08:38:57 +08:00
double beats = ( Math . Max ( time , 0 ) - timingPoint . Time ) / beatLength ;
2021-04-28 16:16:05 +08:00
2023-01-12 08:38:57 +08:00
int roundedBeats = ( int ) Math . Round ( beats , MidpointRounding . AwayFromZero ) ;
double snappedTime = timingPoint . Time + roundedBeats * beatLength ;
if ( snappedTime > = 0 )
return snappedTime ;
return snappedTime + beatLength ;
2021-04-28 15:47:30 +08:00
}
2017-05-24 01:18:25 +08:00
/// <summary>
2017-05-24 01:22:28 +08:00
/// Binary searches one of the control point lists to find the active control point at <paramref name="time"/>.
2019-10-26 07:31:41 +08:00
/// Includes logic for returning a specific point when no matching point is found.
2017-05-24 01:18:25 +08:00
/// </summary>
/// <param name="list">The list to search.</param>
/// <param name="time">The time to find the control point at.</param>
2020-07-18 10:53:04 +08:00
/// <param name="fallback">The control point to use when <paramref name="time"/> is before any control points.</param>
2019-10-26 07:31:41 +08:00
/// <returns>The active control point at <paramref name="time"/>, or a fallback <see cref="ControlPoint"/> if none found.</returns>
2022-10-20 13:34:07 +08:00
public static T BinarySearchWithFallback < T > ( IReadOnlyList < T > list , double time , T fallback )
2022-10-18 15:01:04 +08:00
where T : class , IControlPoint
2019-10-26 07:31:41 +08:00
{
2021-08-25 17:00:57 +08:00
return BinarySearch ( list , time ) ? ? fallback ;
2019-10-26 07:31:41 +08:00
}
/// <summary>
/// Binary searches one of the control point lists to find the active control point at <paramref name="time"/>.
/// </summary>
/// <param name="list">The list to search.</param>
/// <param name="time">The time to find the control point at.</param>
2022-10-20 22:08:18 +08:00
/// <returns>The active control point at <paramref name="time"/>. Will return <c>null</c> if there are no control points, or if the time is before the first control point.</returns>
2022-10-20 13:34:07 +08:00
public static T BinarySearch < T > ( IReadOnlyList < T > list , double time )
2022-10-18 15:01:04 +08:00
where T : class , IControlPoint
2017-05-23 14:20:32 +08:00
{
2022-12-23 04:27:59 +08:00
ArgumentNullException . ThrowIfNull ( list ) ;
2018-04-13 17:19:50 +08:00
2024-08-16 06:15:40 +08:00
int index = BinarySearchUtils . BinarySearch ( list , time , c = > c . Time , EqualitySelection . Rightmost ) ;
2018-04-13 17:19:50 +08:00
2024-08-16 06:15:40 +08:00
if ( index < 0 )
index = ~ index - 1 ;
2018-04-13 17:19:50 +08:00
2024-08-16 06:15:40 +08:00
return index > = 0 ? list [ index ] : null ;
2017-05-23 14:20:32 +08:00
}
2019-10-25 18:48:01 +08:00
2019-10-26 10:38:05 +08:00
/// <summary>
2019-11-17 20:49:36 +08:00
/// Check whether <paramref name="newPoint"/> should be added.
2019-10-26 10:38:05 +08:00
/// </summary>
/// <param name="time">The time to find the timing control point at.</param>
/// <param name="newPoint">A point to be added.</param>
/// <returns>Whether the new point should be added.</returns>
2021-08-25 17:00:57 +08:00
protected virtual bool CheckAlreadyExisting ( double time , ControlPoint newPoint )
2019-10-26 10:38:05 +08:00
{
ControlPoint existing = null ;
switch ( newPoint )
{
2022-06-24 20:25:23 +08:00
case TimingControlPoint :
2019-10-26 10:38:05 +08:00
// Timing points are a special case and need to be added regardless of fallback availability.
2021-08-25 17:00:57 +08:00
existing = BinarySearch ( TimingPoints , time ) ;
2019-10-26 10:38:05 +08:00
break ;
2022-06-24 20:25:23 +08:00
case EffectControlPoint :
2019-10-26 10:38:05 +08:00
existing = EffectPointAt ( time ) ;
break ;
}
2020-04-17 16:10:13 +08:00
return newPoint ? . IsRedundant ( existing ) = = true ;
2019-10-26 10:38:05 +08:00
}
2021-08-25 17:00:57 +08:00
protected virtual void GroupItemAdded ( ControlPoint controlPoint )
2019-10-25 18:48:01 +08:00
{
2019-10-28 10:40:33 +08:00
switch ( controlPoint )
2019-10-25 18:48:01 +08:00
{
case TimingControlPoint typed :
2019-10-28 10:40:33 +08:00
timingPoints . Add ( typed ) ;
2019-10-25 18:48:01 +08:00
break ;
case EffectControlPoint typed :
2019-10-28 10:40:33 +08:00
effectPoints . Add ( typed ) ;
2019-10-25 18:48:01 +08:00
break ;
2021-09-06 21:04:37 +08:00
default :
throw new ArgumentException ( $"A control point of unexpected type {controlPoint.GetType()} was added to this {nameof(ControlPointInfo)}" ) ;
2019-10-25 18:48:01 +08:00
}
2024-06-11 15:15:14 +08:00
raiseControlPointsChanged ( ) ;
2019-10-25 18:48:01 +08:00
}
2021-08-25 17:00:57 +08:00
protected virtual void GroupItemRemoved ( ControlPoint controlPoint )
2019-10-25 18:48:01 +08:00
{
2019-10-28 10:40:33 +08:00
switch ( controlPoint )
2019-10-25 18:48:01 +08:00
{
case TimingControlPoint typed :
2019-10-28 10:40:33 +08:00
timingPoints . Remove ( typed ) ;
2019-10-25 18:48:01 +08:00
break ;
case EffectControlPoint typed :
2019-10-28 10:40:33 +08:00
effectPoints . Remove ( typed ) ;
2019-10-25 18:48:01 +08:00
break ;
}
2024-06-11 15:15:14 +08:00
raiseControlPointsChanged ( ) ;
2019-10-25 18:48:01 +08:00
}
2021-01-04 15:37:07 +08:00
2021-07-19 11:38:22 +08:00
public ControlPointInfo DeepClone ( )
2021-01-04 15:37:07 +08:00
{
2022-12-16 19:18:02 +08:00
var controlPointInfo = ( ControlPointInfo ) Activator . CreateInstance ( GetType ( ) ) ! ;
2021-01-04 15:37:07 +08:00
foreach ( var point in AllControlPoints )
2021-07-19 11:38:22 +08:00
controlPointInfo . Add ( point . Time , point . DeepClone ( ) ) ;
2021-01-04 15:37:07 +08:00
return controlPointInfo ;
}
2017-05-23 12:55:18 +08:00
}
2017-12-05 23:37:37 +08:00
}