// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. using System; using System.Collections.Generic; using System.Linq; using Newtonsoft.Json; using osu.Framework.Lists; namespace osu.Game.Beatmaps.ControlPoints { [Serializable] public class ControlPointInfo { /// /// Control point groups. /// [JsonProperty] public IReadOnlyList Groups => groups; private readonly SortedList groups = new SortedList(Comparer.Default); /// /// All timing points. /// [JsonProperty] public IReadOnlyList TimingPoints => timingPoints; private readonly SortedList timingPoints = new SortedList(Comparer.Default); /// /// All difficulty points. /// [JsonProperty] public IReadOnlyList DifficultyPoints => difficultyPoints; private readonly SortedList difficultyPoints = new SortedList(Comparer.Default); /// /// All sound points. /// [JsonProperty] public IReadOnlyList SamplePoints => samplePoints; private readonly SortedList samplePoints = new SortedList(Comparer.Default); /// /// All effect points. /// [JsonProperty] public IReadOnlyList EffectPoints => effectPoints; private readonly SortedList effectPoints = new SortedList(Comparer.Default); /// /// All control points, of all types. /// public IEnumerable AllControlPoints => Groups.SelectMany(g => g.ControlPoints).ToArray(); /// /// Finds the difficulty control point that is active at . /// /// The time to find the difficulty control point at. /// The difficulty control point. public DifficultyControlPoint DifficultyPointAt(double time) => binarySearch(DifficultyPoints, time); /// /// Finds the effect control point that is active at . /// /// The time to find the effect control point at. /// The effect control point. public EffectControlPoint EffectPointAt(double time) => binarySearch(EffectPoints, time); /// /// Finds the sound control point that is active at . /// /// The time to find the sound control point at. /// The sound control point. public SampleControlPoint SamplePointAt(double time) => binarySearch(SamplePoints, time, SamplePoints.Count > 0 ? SamplePoints[0] : null); /// /// Finds the timing control point that is active at . /// /// The time to find the timing control point at. /// The timing control point. public TimingControlPoint TimingPointAt(double time) => binarySearch(TimingPoints, time, TimingPoints.Count > 0 ? TimingPoints[0] : null); /// /// Finds the closest of the same type as that is active at . /// /// The time to find the timing control point at. /// A reference point to infer type. /// The timing control point. public ControlPoint SimilarPointAt(double time, ControlPoint referencePoint) { switch (referencePoint) { case TimingControlPoint _: return TimingPointAt(time); case EffectControlPoint _: return EffectPointAt(time); case SampleControlPoint _: return SamplePointAt(time); case DifficultyControlPoint _: return DifficultyPointAt(time); } return null; } /// /// Finds the maximum BPM represented by any timing control point. /// [JsonIgnore] public double BPMMaximum => 60000 / (TimingPoints.OrderBy(c => c.BeatLength).FirstOrDefault() ?? new TimingControlPoint()).BeatLength; /// /// Finds the minimum BPM represented by any timing control point. /// [JsonIgnore] public double BPMMinimum => 60000 / (TimingPoints.OrderByDescending(c => c.BeatLength).FirstOrDefault() ?? new TimingControlPoint()).BeatLength; /// /// Finds the mode BPM (most common BPM) represented by the control points. /// [JsonIgnore] public double BPMMode => 60000 / (TimingPoints.GroupBy(c => c.BeatLength).OrderByDescending(grp => grp.Count()).FirstOrDefault()?.FirstOrDefault() ?? new TimingControlPoint()).BeatLength; /// /// Binary searches one of the control point lists to find the active control point at . /// /// The list to search. /// The time to find the control point at. /// The control point to use when is before any control points. If null, a new control point will be constructed. /// The active control point at . private T binarySearch(IReadOnlyList list, double time, T prePoint = null) where T : ControlPoint, new() { if (list == null) throw new ArgumentNullException(nameof(list)); if (list.Count == 0) return new T(); if (time < list[0].Time) return prePoint ?? new T(); if (time >= list[list.Count - 1].Time) return list[list.Count - 1]; int l = 0; int r = list.Count - 2; while (l <= r) { int pivot = l + ((r - l) >> 1); if (list[pivot].Time < time) l = pivot + 1; else if (list[pivot].Time > time) r = pivot - 1; else return list[pivot]; } // l will be the first control point with Time > time, but we want the one before it return list[l - 1]; } public void Add(double time, ControlPoint newPoint, bool force = false) { if (!force && SimilarPointAt(time, newPoint)?.EquivalentTo(newPoint) == true) return; GroupAt(time, true).Add(newPoint); } public ControlPointGroup GroupAt(double time, bool createIfNotExisting) { var existing = Groups.FirstOrDefault(g => g.Time == time); if (existing != null) return existing; if (createIfNotExisting) { var newGroup = new ControlPointGroup(time); newGroup.ItemAdded += groupItemAdded; newGroup.ItemRemoved += groupItemRemoved; groups.Add(newGroup); return newGroup; } return null; } private void groupItemRemoved(ControlPoint obj) { switch (obj) { case TimingControlPoint typed: timingPoints.Remove(typed); break; case EffectControlPoint typed: effectPoints.Remove(typed); break; case SampleControlPoint typed: samplePoints.Remove(typed); break; case DifficultyControlPoint typed: difficultyPoints.Remove(typed); break; } } private void groupItemAdded(ControlPoint obj) { switch (obj) { case TimingControlPoint typed: timingPoints.Add(typed); break; case EffectControlPoint typed: effectPoints.Add(typed); break; case SampleControlPoint typed: samplePoints.Add(typed); break; case DifficultyControlPoint typed: difficultyPoints.Add(typed); break; } } public void Clear() => groups.Clear(); } }