diff --git a/Quaver.API.Tests/Quaver/Resources/stable-sorting.qua b/Quaver.API.Tests/Quaver/Resources/stable-sorting.qua new file mode 100644 index 000000000..619274910 --- /dev/null +++ b/Quaver.API.Tests/Quaver/Resources/stable-sorting.qua @@ -0,0 +1,205 @@ +AudioFile: '' +SongPreviewTime: 0 +BackgroundFile: '' +MapId: -1 +MapSetId: -1 +Mode: Keys4 +Title: '' +Artist: '' +Source: '' +Tags: '' +DifficultyName: '' +EditorLayers: [] +Bookmarks: +- StartTime: 0 + Note: 'first' +- StartTime: 0 + Note: 'first' +- StartTime: 0 + Note: 'first' +- StartTime: 0 + Note: 'first' +- StartTime: 0 + Note: 'first' +- StartTime: 0 + Note: 'first' +- StartTime: 0 + Note: 'first' +- StartTime: 0 + Note: 'first' +- StartTime: 0 + Note: 'first' +- StartTime: 0 + Note: 'first' +- StartTime: 0 + Note: 'first' +- StartTime: 0 + Note: 'first' +- StartTime: 0 + Note: 'second' +- StartTime: 0 + Note: 'third' +- StartTime: 1000 + Note: 'fourth' +- StartTime: 1000 + Note: 'fifth' +- StartTime: 1000 + Note: 'sixth' +CustomAudioSamples: [] +SoundEffects: [] +TimingPoints: +- StartTime: 0 + Bpm: 6 + Signature: 13646 +- StartTime: 0 + Bpm: 6 + Signature: 13646 +- StartTime: 0 + Bpm: 6 + Signature: 13646 +- StartTime: 0 + Bpm: 6 + Signature: 13646 +- StartTime: 0 + Bpm: 6 + Signature: 13646 +- StartTime: 0 + Bpm: 6 + Signature: 13646 +- StartTime: 0 + Bpm: 6 + Signature: 13646 +- StartTime: 0 + Bpm: 6 + Signature: 13646 +- StartTime: 0 + Bpm: 6 + Signature: 13646 +- StartTime: 0 + Bpm: 6 + Signature: 13646 +- StartTime: 0 + Bpm: 6 + Signature: 13646 +- StartTime: 0 + Bpm: 6 + Signature: 13646 +- StartTime: 0 + Bpm: 4 + Signature: 3 +- StartTime: 0 + Bpm: 12 + Signature: 4 +- StartTime: 1000 + Bpm: 27 + Signature: 246 +- StartTime: 1000 + Bpm: 1 + Signature: 12 +- StartTime: 1000 + Bpm: 136 + Signature: 3785 +SliderVelocities: +- StartTime: 0 + Multiplier: 1253 +- StartTime: 0 + Multiplier: 1253 +- StartTime: 0 + Multiplier: 1253 +- StartTime: 0 + Multiplier: 1253 +- StartTime: 0 + Multiplier: 1253 +- StartTime: 0 + Multiplier: 1253 +- StartTime: 0 + Multiplier: 1253 +- StartTime: 0 + Multiplier: 1253 +- StartTime: 0 + Multiplier: 1253 +- StartTime: 0 + Multiplier: 1253 +- StartTime: 0 + Multiplier: 1253 +- StartTime: 0 + Multiplier: 1253 +- StartTime: 0 + Multiplier: 8.4 +- StartTime: 0 + Multiplier: -1345 +- StartTime: 1000 + Multiplier: 675 +- StartTime: 1000 + Multiplier: 0 +- StartTime: 1000 + Multiplier: -13548 +HitObjects: +- StartTime: 0 + Lane: 4 + KeySounds: [] + HitSound: Normal +- StartTime: 0 + Lane: 4 + KeySounds: [] + HitSound: Normal +- StartTime: 0 + Lane: 4 + KeySounds: [] + HitSound: Normal +- StartTime: 0 + Lane: 4 + KeySounds: [] + HitSound: Normal +- StartTime: 0 + Lane: 4 + KeySounds: [] + HitSound: Normal +- StartTime: 0 + Lane: 4 + KeySounds: [] + HitSound: Normal +- StartTime: 0 + Lane: 4 + KeySounds: [] + HitSound: Normal +- StartTime: 0 + Lane: 4 + KeySounds: [] + HitSound: Normal +- StartTime: 0 + Lane: 4 + KeySounds: [] + HitSound: Normal +- StartTime: 0 + Lane: 4 + KeySounds: [] + HitSound: Normal +- StartTime: 0 + Lane: 4 + KeySounds: [] + HitSound: Normal +- StartTime: 0 + Lane: 4 + KeySounds: [] + HitSound: Normal +- StartTime: 0 + Lane: 3 + KeySounds: [] + HitSound: Normal +- StartTime: 0 + Lane: 2 + KeySounds: [] + HitSound: Normal +- StartTime: 1000 + Lane: 7 + KeySounds: [] + HitSound: Normal +- StartTime: 1000 + Lane: 1 + KeySounds: [] + HitSound: Normal +- StartTime: 1000 + Lane: 5 + KeySounds: [] + HitSound: Normal \ No newline at end of file diff --git a/Quaver.API.Tests/Quaver/TestCaseQua.cs b/Quaver.API.Tests/Quaver/TestCaseQua.cs index 8a0ff67b5..3f08131dd 100644 --- a/Quaver.API.Tests/Quaver/TestCaseQua.cs +++ b/Quaver.API.Tests/Quaver/TestCaseQua.cs @@ -4,10 +4,12 @@ using System.IO; using System.Linq; using System.Text; +using Force.DeepCloner; using Quaver.API.Enums; using Quaver.API.Maps; using Quaver.API.Maps.Structures; using Xunit; +using YamlDotNet.Serialization; namespace Quaver.API.Tests.Quaver { @@ -249,6 +251,22 @@ public void InvalidKeySoundIndex() Assert.False(qua.IsValid()); } + [Fact] + public void StableSorting() + { + const string Q = "./Quaver/Resources/stable-sorting.qua"; + var unsorted = new Deserializer().Deserialize(File.ReadAllText(Q)); + var sorted = unsorted.DeepClone(); + sorted.Sort(); + + Assert.Equal(unsorted.TimingPoints, sorted.TimingPoints, TimingPointInfo.ByValueComparer); + Assert.Equal(unsorted.SliderVelocities, sorted.SliderVelocities, SliderVelocityInfo.ByValueComparer); + Assert.Equal(unsorted.HitObjects, sorted.HitObjects, HitObjectInfo.ByValueComparer); + Assert.Equal(unsorted.CustomAudioSamples, sorted.CustomAudioSamples, CustomAudioSampleInfo.ByValueComparer); + Assert.Equal(unsorted.EditorLayers, sorted.EditorLayers, EditorLayerInfo.ByValueComparer); + Assert.Equal(unsorted.Bookmarks, sorted.Bookmarks, BookmarkInfo.ByValueComparer); + } + [Fact] public void SVNormalization() { @@ -282,22 +300,22 @@ public void SVNormalization() // Check that the normalization gives the correct result. var quaDenormalizedNormalized = quaDenormalized.WithNormalizedSVs(); - Assert.True(quaDenormalizedNormalized.EqualByValue(quaNormalized)); + Assert.True(quaDenormalizedNormalized.EqualByValue(quaNormalized), $"Expected {test} to normalize correctly."); // Denormalization can move the first SV (it doesn't matter where to put the InitialScrollVelocity SV). // So check back-and-forth instead of just denormalization. var quaNormalizedDenormalizedNormalized = quaNormalized.WithDenormalizedSVs().WithNormalizedSVs(); - Assert.True(quaNormalizedDenormalizedNormalized.EqualByValue(quaNormalized)); + Assert.True(quaNormalizedDenormalizedNormalized.EqualByValue(quaNormalized), $"Expected {test} to remain the same after denormalization and subsequent normalization."); // Check that serializing and parsing the result does not change it. var bufferDenormalized = Encoding.UTF8.GetBytes(quaDenormalized.Serialize()); var quaDenormalized2 = Qua.Parse(bufferDenormalized, false); - Assert.True(quaDenormalized.EqualByValue(quaDenormalized2)); + Assert.True(quaDenormalized.EqualByValue(quaDenormalized2), $"Expected {test} denormalized to remain the same after serialization and parsing."); var bufferNormalized = Encoding.UTF8.GetBytes(quaNormalized.Serialize()); var quaNormalized2 = Qua.Parse(bufferNormalized, false); - Assert.True(quaNormalized.EqualByValue(quaNormalized2)); + Assert.True(quaNormalized.EqualByValue(quaNormalized2), $"Expected {test} to normalized to remain the same after serialization and parsing."); } } } -} \ No newline at end of file +} diff --git a/Quaver.API/Helpers/ListHelper.cs b/Quaver.API/Helpers/ListHelper.cs new file mode 100644 index 000000000..9e6caaf1d --- /dev/null +++ b/Quaver.API/Helpers/ListHelper.cs @@ -0,0 +1,50 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * Copyright (c) 2017-2018 Swan & The Quaver Team . +*/ + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Reflection.Emit; + +namespace Quaver.API.Helpers +{ + public static class ListHelper + { + private static class Cache + { + public static Converter, T[]> Converter { get; } = typeof(List) + .GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) + .FirstOrDefault(x => x.FieldType == typeof(T[])) is { } method + ? CreateGetter(method) + : x => x.ToArray(); + + private static Converter, T[]> CreateGetter(FieldInfo field) + { + var name = $"{field.DeclaringType?.FullName}.get_{field.Name}"; + var getter = new DynamicMethod(name, typeof(T[]), new[] { typeof(List) }, true); + var il = getter.GetILGenerator(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldfld, field); + il.Emit(OpCodes.Ret); + return (Converter, T[]>)getter.CreateDelegate(typeof(Converter, T[]>)); + } + } + + /// + /// Gets the underlying of the . + /// + /// + /// Be careful when using this method as the cannot safeguard + /// against underlying mutations or out-of-bounds reading within its capacity. + /// + /// + /// + /// + public static T[] GetUnderlyingArray(List list) => Cache.Converter(list); + } +} diff --git a/Quaver.API/Helpers/StartTimeHelper.cs b/Quaver.API/Helpers/StartTimeHelper.cs new file mode 100644 index 000000000..8eb71ed8b --- /dev/null +++ b/Quaver.API/Helpers/StartTimeHelper.cs @@ -0,0 +1,234 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * Copyright (c) 2017-2019 Swan & The Quaver Team . +*/ + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics.Contracts; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using Quaver.API.Maps.Structures; + +namespace Quaver.API.Helpers +{ + public static class StartTimeHelper + { + // If the framework supports it, we want to use Random.Shared since it is much faster. + static readonly Random _rng = Shared()?.GetMethod?.Invoke(null, null) as Random ?? new Random(); + + public static void InsertSorted(this List list, T element) + where T : IStartTime => + list.Insert(IndexAtTime(list, element.StartTime) + 1, element); + + public static void InsertSorted(this List list, IEnumerable elements) + where T : IStartTime + { + // Thanks to @WilliamQiufeng for going through the trouble of benchmarking + // to find the optimal capacity and count for our use case. + const int MaximumCapacity = 128; + + const int MinimumCount = 128; + + // ReSharper disable PossibleMultipleEnumeration + switch (TryCount(elements)) + { + case 0: break; + case 1: + InsertSorted(list, elements.First()); + break; + case { } count when count <= MinimumCount && list.Capacity >= MaximumCapacity: + var capacity = list.Capacity; + + if (capacity - list.Count < count) + list.Capacity = Math.Max(capacity * 2, capacity + count); + + InsertSortedList(list, elements, count); + break; + default: // If the list ends up becoming large, it is no longer worth it to find the insertion. + list.AddRange(elements); + list.HybridSort(); + break; + } + } + + /// + /// Sorts the list. + /// + /// + /// This method is intended to be used on collections that have a decent chance of being nearly sorted. + /// It uses Insertion Sort, but falls back on Quick Sort if that algorithm takes too long. + /// The sorting is stable, meaning that the order for equal elements are preserved. + /// + /// The type of list to sort. + /// The list to sort. + public static void HybridSort(this List list) + where T : IStartTime + { + var maxBacktracking = list.Count * (int)Math.Log(list.Count, 2); + + for (var i = 1; i < list.Count; i++) + { + var j = i; + + while (j > 0 && list[j - 1].StartTime > list[j].StartTime) + { + (list[j], list[j - 1]) = (list[j - 1], list[j]); + j--; + + if (--maxBacktracking > 0) + continue; + + // Insertion Sort is deemed to take too long, let's fall back to Quick Sort. + QuickSort(list, 0, list.Count - 1); + return; + } + } + } + + // Ideally would be IReadOnlyList to indicate no mutation, + // but unfortunately IList doesn't implement IReadOnlyList. + [Pure] + public static int IndexAtTime(this List list, float time) + where T : IStartTime + { + var left = 0; + var right = list.Count - 1; + + while (left <= right) + if (left + ((right - left) / 2) is var mid && list[mid].StartTime <= time) + left = mid + 1; + else + right = mid - 1; + + return right; + } + + [Pure] + public static int IndexAtTimeBefore(this List list, float time) + where T : IStartTime => + IndexAtTime(list, Before(time)); + + [Pure] + public static T AtTime(this List list, float time) + where T : IStartTime + { + var i = list.IndexAtTime(time); + return i is -1 ? default : list[i]; + } + + [Pure] + public static T AtTimeBefore(this List list, float time) + where T : IStartTime => + AtTime(list, Before(time)); + + // Thanks to https://stackoverflow.com/a/10426033 for the implementation. + [Pure] + public static float After(float time) + { + // NaNs and positive infinity map to themselves. + if (float.IsNaN(time) || float.IsPositiveInfinity(time)) + return time; + + // 0.0 and -0.0 both map to the smallest +ve float. + if (time is 0) + return float.Epsilon; + + unsafe + { + // Slightly evil bit hack. + _ = time > 0 ? ++*(int*)&time : --*(int*)&time; + } + + return time; + } + + [Pure] + public static float Before(float time) => -After(-time); + + // For interfaces, indexers are generally more performant, hence the suppression below. + private static void InsertSortedList(List list, IEnumerable elements, int count) + where T : IStartTime + { + switch (elements) + { + case IList e: + for (var i = 0; i < count; i++) + InsertSorted(list, e[i]); + + break; + case IReadOnlyList e: + for (var i = 0; i < count; i++) + InsertSorted(list, e[i]); + + break; + default: + foreach (var e in elements) + InsertSorted(list, e); + + break; + } + } + + // ReSharper disable once CognitiveComplexity SuggestBaseTypeForParameter + private static void QuickSort(List list, int leftIndex, int rightIndex) + where T : IStartTime + { + while (true) + { + var i = leftIndex; + var j = rightIndex; + var pivot = list[_rng.Next(leftIndex, rightIndex + 1)]; + + while (i <= j) + { + while (list[i].StartTime < pivot.StartTime) + i++; + + while (list[j].StartTime > pivot.StartTime) + j--; + + if (i > j) + continue; + + (list[i], list[j]) = (list[j], list[i]); + i++; + j--; + } + + if (leftIndex < j) + QuickSort(list, leftIndex, j); + + if (i >= rightIndex) + break; + + leftIndex = i; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining), Pure] + private static int? TryCount(IEnumerable enumerable) => + enumerable switch + { + string c => c.Length, + ICollection c => c.Count, + ICollection c => c.Count, + IReadOnlyCollection c => c.Count, + _ => null, + }; + + private static PropertyInfo Shared() => + typeof(Random).GetProperty( + nameof(Shared), + BindingFlags.Public | BindingFlags.Static, + Type.DefaultBinder, + typeof(Random), + Type.EmptyTypes, + Array.Empty() + ); + } +} diff --git a/Quaver.API/Maps/Qua.cs b/Quaver.API/Maps/Qua.cs index 887e324cc..6954e66ff 100644 --- a/Quaver.API/Maps/Qua.cs +++ b/Quaver.API/Maps/Qua.cs @@ -11,22 +11,19 @@ using System.Diagnostics; using System.IO; using System.Linq; -using System.Reflection; using System.Text; using Force.DeepCloner; using MonoGame.Extended.Collections; using Quaver.API.Enums; using Quaver.API.Helpers; -using Quaver.API.Maps.Parsers; using Quaver.API.Maps.Processors.Difficulty; using Quaver.API.Maps.Processors.Difficulty.Rulesets.Keys; -using Quaver.API.Maps.Processors.Scoring; using Quaver.API.Maps.Structures; using YamlDotNet.Serialization; namespace Quaver.API.Maps { - [Serializable] + [Serializable] // ReSharper disable CognitiveComplexity CompareOfFloatsByEqualityOperator public class Qua { /// @@ -110,7 +107,7 @@ public class Qua public bool LegacyLNRendering { get; set; } /// - /// Indicates if the BPM changes in affect scroll velocity. + /// Indicates if the BPM changes affect scroll velocity. /// /// If this is set to false, SliderVelocities are in the denormalized format (BPM affects SV), /// and if this is set to true, SliderVelocities are in the normalized format (BPM does not affect SV). @@ -176,7 +173,7 @@ public class Qua /// /// [YamlIgnore] - public int Length => HitObjects.Count == 0 ? 0 : HitObjects.Max(x => Math.Max(x.StartTime, x.EndTime)); + public int Length => HitObjects.Count is 0 ? 0 : MaxObjectTime(); /// /// Integer based seed used for shuffling the lanes when randomize mod is active. @@ -194,46 +191,41 @@ public class Qua /// /// Ctor /// - public Qua() - { - } + public Qua() { } /// /// Returns true if the two maps are equal by value. /// /// the Qua to compare to /// - public bool EqualByValue(Qua other) - { - return AudioFile == other.AudioFile - && SongPreviewTime == other.SongPreviewTime - && BackgroundFile == other.BackgroundFile - && BannerFile == other.BannerFile - && MapId == other.MapId - && MapSetId == other.MapSetId - && Mode == other.Mode - && Title == other.Title - && Artist == other.Artist - && Source == other.Source - && Tags == other.Tags - && Creator == other.Creator - && DifficultyName == other.DifficultyName - && Description == other.Description - && Genre == other.Genre - && TimingPoints.SequenceEqual(other.TimingPoints, TimingPointInfo.ByValueComparer) - && SliderVelocities.SequenceEqual(other.SliderVelocities, SliderVelocityInfo.ByValueComparer) - // ReSharper disable once CompareOfFloatsByEqualityOperator - && InitialScrollVelocity == other.InitialScrollVelocity - && BPMDoesNotAffectScrollVelocity == other.BPMDoesNotAffectScrollVelocity - && LegacyLNRendering == other.LegacyLNRendering - && HasScratchKey == other.HasScratchKey - && HitObjects.SequenceEqual(other.HitObjects, HitObjectInfo.ByValueComparer) - && CustomAudioSamples.SequenceEqual(other.CustomAudioSamples, CustomAudioSampleInfo.ByValueComparer) - && SoundEffects.SequenceEqual(other.SoundEffects, SoundEffectInfo.ByValueComparer) - && EditorLayers.SequenceEqual(other.EditorLayers, EditorLayerInfo.ByValueComparer) - && Bookmarks.SequenceEqual(other.Bookmarks, BookmarkInfo.ByValueComparer) - && RandomizeModifierSeed == other.RandomizeModifierSeed; - } + public bool EqualByValue(Qua other) => + AudioFile == other.AudioFile && + SongPreviewTime == other.SongPreviewTime && + BackgroundFile == other.BackgroundFile && + BannerFile == other.BannerFile && + MapId == other.MapId && + MapSetId == other.MapSetId && + Mode == other.Mode && + Title == other.Title && + Artist == other.Artist && + Source == other.Source && + Tags == other.Tags && + Creator == other.Creator && + DifficultyName == other.DifficultyName && + Description == other.Description && + Genre == other.Genre && + TimingPoints.SequenceEqual(other.TimingPoints, TimingPointInfo.ByValueComparer) && + SliderVelocities.SequenceEqual(other.SliderVelocities, SliderVelocityInfo.ByValueComparer) && + InitialScrollVelocity == other.InitialScrollVelocity && + BPMDoesNotAffectScrollVelocity == other.BPMDoesNotAffectScrollVelocity && + LegacyLNRendering == other.LegacyLNRendering && + HasScratchKey == other.HasScratchKey && + HitObjects.SequenceEqual(other.HitObjects, HitObjectInfo.ByValueComparer) && + CustomAudioSamples.SequenceEqual(other.CustomAudioSamples, CustomAudioSampleInfo.ByValueComparer) && + SoundEffects.SequenceEqual(other.SoundEffects, SoundEffectInfo.ByValueComparer) && + EditorLayers.SequenceEqual(other.EditorLayers, EditorLayerInfo.ByValueComparer) && + Bookmarks.SequenceEqual(other.Bookmarks, BookmarkInfo.ByValueComparer) && + RandomizeModifierSeed == other.RandomizeModifierSeed; /// /// Loads a .qua file from a stream @@ -286,6 +278,27 @@ public static Qua Parse(string path, bool checkValidity = true) /// public string Serialize() { + static HitObjectInfo SerializableHitObject(HitObjectInfo obj) => + new HitObjectInfo + { + EditorLayer = obj.EditorLayer, EndTime = obj.EndTime, + HitSound = obj.HitSound == HitSounds.Normal ? 0 : obj.HitSound, KeySounds = obj + .KeySounds + .Select(x => new KeySoundInfo { Sample = x.Sample, Volume = x.Volume == 100 ? 0 : x.Volume }) + .ToList(), + Lane = obj.Lane, StartTime = obj.StartTime, + }; + + static SoundEffectInfo SerializableSoundEffect(SoundEffectInfo x) => + x.Volume == 100 + ? new SoundEffectInfo { StartTime = x.StartTime, Sample = x.Sample, Volume = 0 } + : x; + + static TimingPointInfo SerializableTimingPoint(TimingPointInfo x) => + x.Signature is TimeSignature.Quadruple + ? new TimingPointInfo { Bpm = x.Bpm, Signature = 0, StartTime = x.StartTime, Hidden = x.Hidden } + : x; + // Sort the object before saving. Sort(); @@ -295,73 +308,16 @@ public string Serialize() var originalSoundEffects = SoundEffects; var originalBookmarks = Bookmarks; - TimingPoints = new List(); - foreach (var tp in originalTimingPoints) - { - if (tp.Signature == TimeSignature.Quadruple) - { - TimingPoints.Add(new TimingPointInfo() - { - Bpm = tp.Bpm, - Signature = 0, - StartTime = tp.StartTime, - Hidden = tp.Hidden - }); - } - else - { - TimingPoints.Add(tp); - } - } - - HitObjects = new List(); - foreach (var obj in originalHitObjects) - { - var keySoundsWithDefaults = new List(); - foreach (var keySound in obj.KeySounds) - { - keySoundsWithDefaults.Add(new KeySoundInfo - { - Sample = keySound.Sample, - Volume = keySound.Volume == 100 ? 0 : keySound.Volume - }); - } - - HitObjects.Add(new HitObjectInfo() - { - EndTime = obj.EndTime, - HitSound = obj.HitSound == HitSounds.Normal ? 0 : obj.HitSound, - KeySounds = keySoundsWithDefaults, - Lane = obj.Lane, - StartTime = obj.StartTime, - EditorLayer = obj.EditorLayer - }); - } - - SoundEffects = new List(); - foreach (var info in originalSoundEffects) - { - if (info.Volume == 100) - { - SoundEffects.Add(new SoundEffectInfo() - { - StartTime = info.StartTime, - Sample = info.Sample, - Volume = 0 - }); - } - else - { - SoundEffects.Add(info); - } - } + TimingPoints = originalTimingPoints.Select(SerializableTimingPoint).ToList(); + HitObjects = originalHitObjects.Select(SerializableHitObject).ToList(); + SoundEffects = originalSoundEffects.Select(SerializableSoundEffect).ToList(); // Doing this to keep compatibility with older versions of .qua (.osu and .sm file conversions). It won't serialize // the bookmarks in the file. if (Bookmarks.Count == 0) Bookmarks = null; - var serializer = new Serializer(); + var serializer = new Serializer(); // ReSharper disable once UsingStatementResourceInitialization using var stringWriter = new StringWriter { NewLine = "\r\n" }; serializer.Serialize(stringWriter, this); var serialized = stringWriter.ToString(); @@ -447,11 +403,11 @@ public List Validate() /// public void Sort() { - HitObjects = HitObjects.OrderBy(x => x.StartTime).ToList(); - TimingPoints = TimingPoints.OrderBy(x => x.StartTime).ToList(); - SliderVelocities = SliderVelocities.OrderBy(x => x.StartTime).ToList(); - SoundEffects = SoundEffects.OrderBy(x => x.StartTime).ToList(); - Bookmarks = Bookmarks.OrderBy(x => x.StartTime).ToList(); + SortBookmarks(); + SortHitObjects(); + SortSoundEffects(); + SortTimingPoints(); + SortSliderVelocities(); } /// @@ -518,27 +474,14 @@ public float GetActionsPerSecond(float rate = 1.0f) /// This translates mode to key count. /// /// - public int GetKeyCount(bool includeScratch = true) - { - int count; - - switch (Mode) + public int GetKeyCount(bool includeScratch = true) => + Mode switch { - case GameMode.Keys4: - count = 4; - break; - case GameMode.Keys7: - count = 7; - break; - default: - throw new InvalidEnumArgumentException(); - } - - if (HasScratchKey && includeScratch) - count++; - - return count; - } + GameMode.Keys4 => 4, + GameMode.Keys7 => 7, + _ => throw new InvalidEnumArgumentException(), + } + + (HasScratchKey && includeScratch ? 1 : 0); /// /// Finds the most common BPM in a Qua object. @@ -546,17 +489,17 @@ public int GetKeyCount(bool includeScratch = true) /// public float GetCommonBpm() { - if (TimingPoints.Count == 0) + if (TimingPoints.Count is 0) return 0; // This fallback isn't really justified, but it's only used for tests. - if (HitObjects.Count == 0) + if (HitObjects.Count is 0) return TimingPoints[0].Bpm; var lastObject = HitObjects.OrderByDescending(x => x.IsLongNote ? x.EndTime : x.StartTime).First(); double lastTime = lastObject.IsLongNote ? lastObject.EndTime : lastObject.StartTime; - var durations = new Dictionary(); + for (var i = TimingPoints.Count - 1; i >= 0; i--) { var point = TimingPoints[i]; @@ -568,16 +511,12 @@ public float GetCommonBpm() var duration = (int)(lastTime - (i == 0 ? 0 : point.StartTime)); lastTime = point.StartTime; - if (durations.ContainsKey(point.Bpm)) + if (!durations.TryAdd(point.Bpm, duration)) durations[point.Bpm] += duration; - else - durations[point.Bpm] = duration; } - if (durations.Count == 0) - return TimingPoints[0].Bpm; // osu! hangs on loading the map in this case; we return a sensible result. - - return durations.OrderByDescending(x => x.Value).First().Key; + // osu! hangs on loading the map in this case; we return a sensible result. + return durations.Count is 0 ? TimingPoints[0].Bpm : durations.OrderByDescending(x => x.Value).First().Key; } /// @@ -587,14 +526,11 @@ public float GetCommonBpm() /// public TimingPointInfo GetTimingPointAt(double time) { - var index = TimingPoints.FindLastIndex(x => x.StartTime <= time); + var index = TimingPoints.IndexAtTime((float)time); // If the point can't be found, we want to return either null if there aren't - // any points, or the first timing point, since it'll be considered as apart of it anyway. - if (index == -1) - return TimingPoints.Count == 0 ? null : TimingPoints.First(); - - return TimingPoints[index]; + // any points, or the first timing point, since it'll be considered as a part of it anyway. + return index == -1 ? TimingPoints.Count is 0 ? null : TimingPoints[0] : TimingPoints[index]; } /// @@ -602,22 +538,14 @@ public TimingPointInfo GetTimingPointAt(double time) /// /// /// - public BookmarkInfo GetBookmarkAt(int time) - { - var index = Bookmarks.FindIndex(b => b.StartTime == time); - return index == -1 ? null : Bookmarks[index]; - } + public BookmarkInfo GetBookmarkAt(int time) => Bookmarks.AtTime(time); /// /// Gets a scroll velocity at a particular time in the map /// /// /// - public SliderVelocityInfo GetScrollVelocityAt(double time) - { - var index = SliderVelocities.FindLastIndex(x => x.StartTime <= time); - return index == -1 ? null : SliderVelocities[index]; - } + public SliderVelocityInfo GetScrollVelocityAt(double time) => SliderVelocities.AtTime((float)time); /// /// Finds the length of a timing point. @@ -653,21 +581,19 @@ public DifficultyProcessor SolveDifficulty(ModIdentifier mods = ModIdentifier.No var qua = this; // Create a new version of the qua with modifiers applied, and use that for calculations. + // ReSharper disable once InvertIf if (applyMods) { qua = qua.DeepClone(); qua.ApplyMods(mods); } - switch (Mode) + return Mode switch { - case GameMode.Keys4: - return new DifficultyProcessorKeys(qua, new StrainConstantsKeys(), mods); - case GameMode.Keys7: - return new DifficultyProcessorKeys(qua, new StrainConstantsKeys(), mods); - default: - throw new InvalidEnumArgumentException(); - } + GameMode.Keys4 => new DifficultyProcessorKeys(qua, new StrainConstantsKeys(), mods), + GameMode.Keys7 => new DifficultyProcessorKeys(qua, new StrainConstantsKeys(), mods), + _ => throw new InvalidEnumArgumentException(), + }; } /// @@ -687,16 +613,20 @@ public double SVFactor() // Create a list of important timestamps from the perspective of playing the map. var importantTimestamps = new List(); + foreach (var hitObject in HitObjects) { importantTimestamps.Add(hitObject.StartTime); + if (hitObject.IsLongNote) importantTimestamps.Add(hitObject.EndTime); } + importantTimestamps.Sort(); var nextImportantTimestampIndex = 0; var sum = 0d; + for (var i = 1; i < qua.SliderVelocities.Count; i++) { var prevSv = qua.SliderVelocities[i - 1]; @@ -704,7 +634,7 @@ public double SVFactor() // Find the first important timestamp after the SV. while (nextImportantTimestampIndex < importantTimestamps.Count && - importantTimestamps[nextImportantTimestampIndex] < sv.StartTime) + importantTimestamps[nextImportantTimestampIndex] < sv.StartTime) nextImportantTimestampIndex++; // Don't count the SV if there's nothing important within 1 second after it. @@ -735,12 +665,8 @@ public double SVFactor() /// public void ReplaceLongNotesWithRegularNotes() { - for (var i = 0; i < HitObjects.Count; i++) - { - var temp = HitObjects[i]; + foreach (var temp in HitObjects) temp.EndTime = 0; - HitObjects[i] = temp; - } } /// @@ -755,7 +681,7 @@ public void ApplyInverse() // // Ideally this should be computed in a smart way using the judgements so that it is always possible to get // perfects, but making map mods depend on the judgements (affected by strict/chill/accuracy adjustments) is - // a really bad idea. I'm setting these to values that will probably work fine for the majority of the + // a terrible idea. I'm setting these to values that will probably work fine for the majority of the // cases. const int MINIMAL_LN_LENGTH = 36; const int MINIMAL_GAP_LENGTH = 36; @@ -766,6 +692,7 @@ public void ApplyInverse() // An array indicating whether the currently processed HitObject is the first in its lane. var firstInLane = new bool[keyCount]; + for (var i = 0; i < firstInLane.Length; i++) firstInLane[i] = true; @@ -782,21 +709,18 @@ public void ApplyInverse() // Find the next and second next hit object in the lane. HitObjectInfo nextObjectInLane = null, secondNextObjectInLane = null; + for (var j = i + 1; j < HitObjects.Count; j++) - { if (HitObjects[j].Lane == currentObject.Lane) { if (nextObjectInLane == null) - { nextObjectInLane = HitObjects[j]; - } else { secondNextObjectInLane = HitObjects[j]; break; } } - } var isFirstInLane = firstInLane[currentObject.Lane - 1]; firstInLane[currentObject.Lane - 1] = false; @@ -810,6 +734,7 @@ public void ApplyInverse() // Figure out the time gap between the end of the LN which we'll create and the next object. int? timeGap = null; + if (nextObjectInLane != null) { var timingPoint = GetTimingPointAt(nextObjectInLane.StartTime); @@ -821,7 +746,7 @@ public void ApplyInverse() // should use the fast section's BPM. if ((int)Math.Round(timingPoint.StartTime) == nextObjectInLane.StartTime) { - var prevTimingPointIndex = TimingPoints.FindLastIndex(x => x.StartTime < timingPoint.StartTime); + var prevTimingPointIndex = TimingPoints.IndexAtTimeBefore(timingPoint.StartTime); // No timing points before the object? Just use the first timing point then, it has the correct // BPM. @@ -831,9 +756,7 @@ public void ApplyInverse() bpm = TimingPoints[prevTimingPointIndex].Bpm; } else - { bpm = timingPoint.Bpm; - } // The time gap is quarter of the milliseconds per beat. timeGap = (int?)Math.Max(Math.Round(15000 / bpm), MINIMAL_GAP_LENGTH); @@ -868,21 +791,19 @@ public void ApplyInverse() // Clear the keysounds as we're moving the start, so they won't make sense. currentObject.KeySounds = new List(); - // If the next object is not an LN and it's the last object in the lane, or if it's an LN and + // If the next object is not an LN, and it's the last object in the lane, or if it's an LN and // not the last object in the lane, create a regular object at the next object's start position. if ((secondNextObjectInLane == null) != nextObjectInLane.IsLongNote) currentObject.EndTime = nextObjectInLane.StartTime; // Filter out really short LNs or even negative length resulting from jacks or weird BPM values. if (currentObject.EndTime - currentObject.StartTime < MINIMAL_LN_LENGTH) - { // These get skipped entirely. // // Actually, there can be a degenerate pattern of multiple LNs with really short gaps // in between them (less than MINIMAL_LN_LENGTH), which this logic will convert // into nothing. That should be pretty rare though. continue; - } } } else @@ -890,27 +811,21 @@ public void ApplyInverse() // Regular objects are replaced with LNs starting from their start and ending quarter of a beat // before the next object's start. if (nextObjectInLane == null) - { // If this is the last object in lane, though, then it's not included, and instead the previous // LN spans up to this object's StartTime. continue; - } currentObject.EndTime = nextObjectInLane.StartTime - timeGap.Value; - // If the next object is not an LN and it's the last object in the lane, or if it's an LN and + // If the next object is not an LN, and it's the last object in the lane, or if it's an LN and // not the last object in the lane, this LN should span until its start. if ((secondNextObjectInLane == null) == (nextObjectInLane.EndTime == 0)) - { currentObject.EndTime = nextObjectInLane.StartTime; - } // Filter out really short LNs or even negative length resulting from jacks or weird BPM values. if (currentObject.EndTime - currentObject.StartTime < MINIMAL_LN_LENGTH) - { // These get converted back into regular objects. currentObject.EndTime = 0; - } } newHitObjects.Add(currentObject); @@ -918,7 +833,8 @@ public void ApplyInverse() // LN conversion can mess up the ordering, so sort it again. See the (this part can mess up the ordering) // comment above. - HitObjects = newHitObjects.OrderBy(x => x.StartTime).ToList(); + newHitObjects.HybridSort(); + HitObjects = newHitObjects; } /// @@ -958,18 +874,13 @@ public void RandomizeLanes(int seed) var values = new List(); values.AddRange(Enumerable.Range(0, GetKeyCount(false)).Select(x => x + 1)); - values.Shuffle(new Random(seed)); if (HasScratchKey) values.Add(GetKeyCount()); - for (var i = 0; i < HitObjects.Count; i++) - { - var temp = HitObjects[i]; + foreach (var temp in HitObjects) temp.Lane = values[temp.Lane - 1]; - HitObjects[i] = temp; - } } /// @@ -979,33 +890,37 @@ public void MirrorHitObjects() { var keyCount = GetKeyCount(); - for (var i = 0; i < HitObjects.Count; i++) - { - var temp = HitObjects[i]; - - if (HasScratchKey) + if (HasScratchKey) // ReSharper disable once ForeachCanBePartlyConvertedToQueryUsingAnotherGetEnumerator + foreach (var temp in HitObjects) { // The scratch lane (which is the last lane in Quaver) should not be mirrored. - if (temp.Lane == keyCount) - continue; - temp.Lane = keyCount - temp.Lane; + if (temp.Lane != keyCount) + temp.Lane = keyCount - temp.Lane; } - else - { + else + foreach (var temp in HitObjects) temp.Lane = keyCount - temp.Lane + 1; - } - - HitObjects[i] = temp; - } } /// /// - public void SortSliderVelocities() => SliderVelocities = SliderVelocities.OrderBy(x => x.StartTime).ToList(); + public void SortBookmarks() => Bookmarks.HybridSort(); + + /// + /// + public void SortHitObjects() => HitObjects.HybridSort(); + + /// + /// + public void SortSliderVelocities() => SliderVelocities.HybridSort(); /// /// - public void SortTimingPoints() => TimingPoints = TimingPoints.OrderBy(x => x.StartTime).ToList(); + public void SortSoundEffects() => SoundEffects.HybridSort(); + + /// + /// + public void SortTimingPoints() => TimingPoints.HybridSort(); /// /// Gets the judgement of a particular hitobject in the map @@ -1014,22 +929,17 @@ public void MirrorHitObjects() /// public int GetHitObjectJudgementIndex(HitObjectInfo ho) { - var index = -1; - var total = 0; - for (var i = 0; i < HitObjects.Count; i++) + foreach (var h in HitObjects) { - if (HitObjects[i] == ho) + if (h == ho) return total; - if (HitObjects[i].IsLongNote) - total += 2; - else - total += 1; + total += h.IsLongNote ? 2 : 1; } - return index; + return -1; } /// @@ -1039,31 +949,14 @@ public int GetHitObjectJudgementIndex(HitObjectInfo ho) /// public HitObjectInfo GetHitObjectAtJudgementIndex(int index) { - HitObjectInfo h = null; - var total = 0; - for (var i = 0; i < HitObjects.Count; i++) - { - total += 1; + // ReSharper disable once ForeachCanBeConvertedToQueryUsingAnotherGetEnumerator + foreach (var h in HitObjects) + if (total++ == index || (h.IsLongNote && total++ == index)) + return h; - if (total - 1 == index) - { - h = HitObjects[i]; - break; - } - - if (HitObjects[i].IsLongNote) - total += 1; - - if (total - 1 == index) - { - h = HitObjects[i]; - break; - } - } - - return h; + return null; } /// @@ -1075,8 +968,10 @@ public static void RestoreDefaultValues(Qua qua) for (var i = 0; i < qua.TimingPoints.Count; i++) { var tp = qua.TimingPoints[i]; + if (tp.Signature == 0) tp.Signature = TimeSignature.Quadruple; + qua.TimingPoints[i] = tp; } @@ -1087,6 +982,7 @@ public static void RestoreDefaultValues(Qua qua) if (obj.HitSound == 0) obj.HitSound = HitSounds.Normal; + // ReSharper disable once ForeachCanBePartlyConvertedToQueryUsingAnotherGetEnumerator foreach (var keySound in obj.KeySounds) if (keySound.Volume == 0) keySound.Volume = 100; @@ -1097,8 +993,10 @@ public static void RestoreDefaultValues(Qua qua) for (var i = 0; i < qua.SoundEffects.Count; i++) { var info = qua.SoundEffects[i]; + if (info.Volume == 0) info.Volume = 100; + qua.SoundEffects[i] = info; } } @@ -1110,12 +1008,8 @@ public static void RestoreDefaultValues(Qua qua) /// private static void AfterLoad(Qua qua, bool checkValidity) { - if (checkValidity) - { - var errors = qua.Validate(); - if (errors.Count > 0) - throw new ArgumentException(string.Join("\n", errors)); - } + if (checkValidity && qua.Validate() is var errors && errors.Count > 0) + throw new ArgumentException(string.Join("\n", errors)); // Try to sort the Qua before returning. qua.Sort(); @@ -1147,9 +1041,8 @@ public void NormalizeSVs() { var timingPoint = TimingPoints[i]; - var nextTimingPointHasSameTimestamp = false; - if (i + 1 < TimingPoints.Count && TimingPoints[i + 1].StartTime == timingPoint.StartTime) - nextTimingPointHasSameTimestamp = true; + var nextTimingPointHasSameTimestamp = i + 1 < TimingPoints.Count && + TimingPoints[i + 1].StartTime == timingPoint.StartTime; while (true) { @@ -1157,6 +1050,7 @@ public void NormalizeSVs() break; var sv = SliderVelocities[currentSvIndex]; + if (sv.StartTime > timingPoint.StartTime) break; @@ -1178,11 +1072,10 @@ public void NormalizeSVs() // ReSharper disable once CompareOfFloatsByEqualityOperator if (multiplier != currentAdjustedSvMultiplier.Value) { - normalizedScrollVelocities.Add(new SliderVelocityInfo - { - StartTime = sv.StartTime, - Multiplier = multiplier, - }); + normalizedScrollVelocities.Add( + new SliderVelocityInfo { StartTime = sv.StartTime, Multiplier = multiplier } + ); + currentAdjustedSvMultiplier = multiplier; } } @@ -1208,15 +1101,14 @@ public void NormalizeSVs() } // ReSharper disable once CompareOfFloatsByEqualityOperator - if (multiplierToo != currentAdjustedSvMultiplier.Value) - { - normalizedScrollVelocities.Add(new SliderVelocityInfo - { - StartTime = timingPoint.StartTime, - Multiplier = multiplierToo, - }); - currentAdjustedSvMultiplier = multiplierToo; - } + if (multiplierToo == currentAdjustedSvMultiplier.Value) + continue; + + normalizedScrollVelocities.Add( + new SliderVelocityInfo { StartTime = timingPoint.StartTime, Multiplier = multiplierToo } + ); + + currentAdjustedSvMultiplier = multiplierToo; } for (; currentSvIndex < SliderVelocities.Count; currentSvIndex++) @@ -1225,21 +1117,22 @@ public void NormalizeSVs() var multiplier = sv.Multiplier * (currentBpm / baseBpm); Debug.Assert(currentAdjustedSvMultiplier != null, nameof(currentAdjustedSvMultiplier) + " != null"); + // ReSharper disable once CompareOfFloatsByEqualityOperator - if (multiplier != currentAdjustedSvMultiplier.Value) - { - normalizedScrollVelocities.Add(new SliderVelocityInfo - { - StartTime = sv.StartTime, - Multiplier = multiplier, - }); - currentAdjustedSvMultiplier = multiplier; - } + if (multiplier == currentAdjustedSvMultiplier.Value) + continue; + + normalizedScrollVelocities.Add( + new SliderVelocityInfo { StartTime = sv.StartTime, Multiplier = multiplier } + ); + + currentAdjustedSvMultiplier = multiplier; } BPMDoesNotAffectScrollVelocity = true; - InitialScrollVelocity = initialSvMultiplier ?? 1; + normalizedScrollVelocities.HybridSort(); SliderVelocities = normalizedScrollVelocities; + InitialScrollVelocity = initialSvMultiplier ?? 1; } /// @@ -1270,12 +1163,14 @@ public void DenormalizeSVs() for (var i = 0; i < TimingPoints.Count; i++) { var timingPoint = TimingPoints[i]; + while (true) { if (currentSvIndex >= SliderVelocities.Count) break; var sv = SliderVelocities[currentSvIndex]; + if (sv.StartTime > timingPoint.StartTime) break; @@ -1288,20 +1183,19 @@ public void DenormalizeSVs() { // ReSharper disable once CompareOfFloatsByEqualityOperator if (currentAdjustedSvMultiplier == null && sv.Multiplier != InitialScrollVelocity) - { // Insert an SV 1 ms earlier to simulate the initial scroll speed multiplier. - denormalizedScrollVelocities.Add(new SliderVelocityInfo - { - StartTime = sv.StartTime - 1, - Multiplier = InitialScrollVelocity / (currentBpm / baseBpm), - }); - } - - denormalizedScrollVelocities.Add(new SliderVelocityInfo - { - StartTime = sv.StartTime, - Multiplier = multiplier, - }); + denormalizedScrollVelocities.Add( + new SliderVelocityInfo + { + StartTime = sv.StartTime - 1, + Multiplier = InitialScrollVelocity / (currentBpm / baseBpm), + } + ); + + denormalizedScrollVelocities.Add( + new SliderVelocityInfo { StartTime = sv.StartTime, Multiplier = multiplier } + ); + currentAdjustedSvMultiplier = multiplier; } } @@ -1319,14 +1213,14 @@ public void DenormalizeSVs() // ReSharper disable once CompareOfFloatsByEqualityOperator if (currentAdjustedSvMultiplier == null && currentSvMultiplier != InitialScrollVelocity) - { // Insert an SV 1 ms earlier to simulate the initial scroll speed multiplier. - denormalizedScrollVelocities.Add(new SliderVelocityInfo - { - StartTime = timingPoint.StartTime - 1, - Multiplier = InitialScrollVelocity / (currentBpm / baseBpm), - }); - } + denormalizedScrollVelocities.Add( + new SliderVelocityInfo + { + StartTime = timingPoint.StartTime - 1, + Multiplier = InitialScrollVelocity / (currentBpm / baseBpm), + } + ); // Timing points reset the SV multiplier. currentAdjustedSvMultiplier = 1; @@ -1340,15 +1234,14 @@ public void DenormalizeSVs() var multiplierToo = currentSvMultiplier / (currentBpm / baseBpm); // ReSharper disable once CompareOfFloatsByEqualityOperator - if (multiplierToo != currentAdjustedSvMultiplier.Value) - { - denormalizedScrollVelocities.Add(new SliderVelocityInfo - { - StartTime = timingPoint.StartTime, - Multiplier = multiplierToo, - }); - currentAdjustedSvMultiplier = multiplierToo; - } + if (multiplierToo == currentAdjustedSvMultiplier.Value) + continue; + + denormalizedScrollVelocities.Add( + new SliderVelocityInfo { StartTime = timingPoint.StartTime, Multiplier = multiplierToo } + ); + + currentAdjustedSvMultiplier = multiplierToo; } for (; currentSvIndex < SliderVelocities.Count; currentSvIndex++) @@ -1357,20 +1250,21 @@ public void DenormalizeSVs() var multiplier = sv.Multiplier / (currentBpm / baseBpm); Debug.Assert(currentAdjustedSvMultiplier != null, nameof(currentAdjustedSvMultiplier) + " != null"); + // ReSharper disable once CompareOfFloatsByEqualityOperator - if (multiplier != currentAdjustedSvMultiplier.Value) - { - denormalizedScrollVelocities.Add(new SliderVelocityInfo - { - StartTime = sv.StartTime, - Multiplier = multiplier, - }); - currentAdjustedSvMultiplier = multiplier; - } + if (multiplier == currentAdjustedSvMultiplier.Value) + continue; + + denormalizedScrollVelocities.Add( + new SliderVelocityInfo { StartTime = sv.StartTime, Multiplier = multiplier } + ); + + currentAdjustedSvMultiplier = multiplier; } - BPMDoesNotAffectScrollVelocity = false; InitialScrollVelocity = 0; + BPMDoesNotAffectScrollVelocity = false; + denormalizedScrollVelocities.HybridSort(); SliderVelocities = denormalizedScrollVelocities; } @@ -1410,24 +1304,33 @@ public Qua WithDenormalizedSVs() /// public string GetBannerPath() => GetFullPath(BannerFile); - private string GetFullPath(string file) - { - if (string.IsNullOrEmpty(file) || string.IsNullOrEmpty(FilePath)) - return null; - - return $"{Path.GetDirectoryName(FilePath)}/{file}"; - } + private string GetFullPath(string file) => + string.IsNullOrEmpty(file) || string.IsNullOrEmpty(FilePath) + ? null + : Path.Join(Path.GetDirectoryName(FilePath.AsSpan()), file); /// /// Returns the path of the audio track file. If no track exists, it will return null. /// /// - public string GetAudioPath() - { - if (string.IsNullOrEmpty(AudioFile) || string.IsNullOrEmpty(FilePath)) - return null; + public string GetAudioPath() => GetFullPath(AudioFile); - return $"{Path.GetDirectoryName(FilePath)}/{AudioFile}"; + private int MaxObjectTime() + { + Debug.Assert(HitObjects.Count != 0, "HitObjects.Count != 0"); + var max = HitObjects[^1].StartTime; + var span = ListHelper.GetUnderlyingArray(HitObjects).AsSpan(0, HitObjects.Count); + + // Incredibly niche micro-optimization: CPUs are able to perform better branch prediction when this is done + // backwards because matches are likely to only ever occur at the end of the span, which means that the CPU + // will default to predicting false after the first few loops, which is almost always correct. Theoretically + // this could be even faster if we implement SIMD, but since this requires far more rewriting than just a + // for-loop, I will leave it as is until we specifically require this function to be as fast as possible. + for (var i = span.Length - 1; i >= 0; i--) + if (span[i].EndTime is var end && end > max) + max = end; + + return max; } } } diff --git a/Quaver.API/Maps/Structures/BookmarkInfo.cs b/Quaver.API/Maps/Structures/BookmarkInfo.cs index 8a358c5cc..6cf0bb97b 100644 --- a/Quaver.API/Maps/Structures/BookmarkInfo.cs +++ b/Quaver.API/Maps/Structures/BookmarkInfo.cs @@ -1,3 +1,10 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * Copyright (c) 2017-2019 Swan & The Quaver Team . +*/ + using System; using System.Collections.Generic; using MoonSharp.Interpreter; @@ -7,7 +14,7 @@ namespace Quaver.API.Maps.Structures { [MoonSharpUserData] [Serializable] - public class BookmarkInfo + public class BookmarkInfo : IStartTime { public int StartTime { @@ -23,6 +30,12 @@ public string Note set; } + float IStartTime.StartTime + { + get => StartTime; + set => StartTime = (int)value; + } + private sealed class TimeNoteEqualityComparer : IEqualityComparer { public bool Equals(BookmarkInfo x, BookmarkInfo y) @@ -31,15 +44,13 @@ public bool Equals(BookmarkInfo x, BookmarkInfo y) if (ReferenceEquals(x, null)) return false; if (ReferenceEquals(y, null)) return false; if (x.GetType() != y.GetType()) return false; + return x.StartTime == y.StartTime && x.Note == y.Note; } - public int GetHashCode(BookmarkInfo obj) - { - return HashCode.Combine(obj.StartTime, obj.Note); - } + public int GetHashCode(BookmarkInfo obj) => HashCode.Combine(obj.StartTime, obj.Note); } public static IEqualityComparer ByValueComparer { get; } = new TimeNoteEqualityComparer(); } -} \ No newline at end of file +} diff --git a/Quaver.API/Maps/Structures/CustomAudioSampleInfo.cs b/Quaver.API/Maps/Structures/CustomAudioSampleInfo.cs index dcc24ff1e..76ca1726e 100644 --- a/Quaver.API/Maps/Structures/CustomAudioSampleInfo.cs +++ b/Quaver.API/Maps/Structures/CustomAudioSampleInfo.cs @@ -37,6 +37,7 @@ public bool Equals(CustomAudioSampleInfo x, CustomAudioSampleInfo y) if (ReferenceEquals(x, null)) return false; if (ReferenceEquals(y, null)) return false; if (x.GetType() != y.GetType()) return false; + return string.Equals(x.Path, y.Path) && x.UnaffectedByRate == y.UnaffectedByRate; } @@ -51,4 +52,4 @@ public int GetHashCode(CustomAudioSampleInfo obj) public static IEqualityComparer ByValueComparer { get; } = new ByValueEqualityComparer(); } -} \ No newline at end of file +} diff --git a/Quaver.API/Maps/Structures/EditorLayerInfo.cs b/Quaver.API/Maps/Structures/EditorLayerInfo.cs index 072317749..94be5c76a 100644 --- a/Quaver.API/Maps/Structures/EditorLayerInfo.cs +++ b/Quaver.API/Maps/Structures/EditorLayerInfo.cs @@ -1,4 +1,11 @@ -using System; +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * Copyright (c) 2017-2019 Swan & The Quaver Team . +*/ + +using System; using System.Collections.Generic; using System.Drawing; using MoonSharp.Interpreter; @@ -50,6 +57,7 @@ public bool Equals(EditorLayerInfo x, EditorLayerInfo y) if (ReferenceEquals(x, null)) return false; if (ReferenceEquals(y, null)) return false; if (x.GetType() != y.GetType()) return false; + return string.Equals(x.Name, y.Name) && x.Hidden == y.Hidden && string.Equals(x.ColorRgb, y.ColorRgb); } diff --git a/Quaver.API/Maps/Structures/HitObjectInfo.cs b/Quaver.API/Maps/Structures/HitObjectInfo.cs index 19490dfd1..138aee157 100644 --- a/Quaver.API/Maps/Structures/HitObjectInfo.cs +++ b/Quaver.API/Maps/Structures/HitObjectInfo.cs @@ -11,17 +11,17 @@ using MoonSharp.Interpreter; using MoonSharp.Interpreter.Interop; using Quaver.API.Enums; +using Quaver.API.Helpers; using YamlDotNet.Serialization; namespace Quaver.API.Maps.Structures { - /// /// HitObjects section of the .qua /// [MoonSharpUserData] [Serializable] - public class HitObjectInfo + public class HitObjectInfo : IStartTime { /// /// The time in milliseconds when the HitObject is supposed to be hit. @@ -46,11 +46,12 @@ public int Lane /// /// The endtime of the HitObject (if greater than 0, it's considered a hold note.) /// - public int EndTime + public int EndTime { get; [MoonSharpVisible(false)] set; } + + float IStartTime.StartTime { - get; - [MoonSharpVisible(false)] - set; + get => StartTime; + set => StartTime = (int)value; } /// @@ -100,17 +101,8 @@ public bool IsEditableInLuaScript /// Gets the timing point this object is in range of. /// /// - public TimingPointInfo GetTimingPoint(List timingPoints) - { - // Search through the entire list for the correct point - for (var i = timingPoints.Count - 1; i >= 0; i--) - { - if (StartTime >= timingPoints[i].StartTime) - return timingPoints[i]; - } - - return timingPoints.First(); - } + public TimingPointInfo GetTimingPoint(List timingPoints) => + timingPoints.AtTime(StartTime) ?? timingPoints[0]; /// /// @@ -169,7 +161,13 @@ public bool Equals(HitObjectInfo x, HitObjectInfo y) if (ReferenceEquals(x, null)) return false; if (ReferenceEquals(y, null)) return false; if (x.GetType() != y.GetType()) return false; - return x.StartTime == y.StartTime && x.Lane == y.Lane && x.EndTime == y.EndTime && x.HitSound == y.HitSound && x.KeySounds.SequenceEqual(y.KeySounds, KeySoundInfo.ByValueComparer) && x.EditorLayer == y.EditorLayer; + + return x.StartTime == y.StartTime && + x.Lane == y.Lane && + x.EndTime == y.EndTime && + x.HitSound == y.HitSound && + x.KeySounds.SequenceEqual(y.KeySounds, KeySoundInfo.ByValueComparer) && + x.EditorLayer == y.EditorLayer; } public int GetHashCode(HitObjectInfo obj) @@ -180,8 +178,10 @@ public int GetHashCode(HitObjectInfo obj) hashCode = (hashCode * 397) ^ obj.Lane; hashCode = (hashCode * 397) ^ obj.EndTime; hashCode = (hashCode * 397) ^ (int)obj.HitSound; + foreach (var keySound in obj.KeySounds) hashCode = (hashCode * 397) ^ KeySoundInfo.ByValueComparer.GetHashCode(keySound); + hashCode = (hashCode * 397) ^ obj.EditorLayer; return hashCode; } diff --git a/Quaver.API/Maps/Structures/IStartTime.cs b/Quaver.API/Maps/Structures/IStartTime.cs new file mode 100644 index 000000000..50a827fdf --- /dev/null +++ b/Quaver.API/Maps/Structures/IStartTime.cs @@ -0,0 +1,16 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * Copyright (c) 2017-2019 Swan & The Quaver Team . +*/ + +using MoonSharp.Interpreter.Interop; + +namespace Quaver.API.Maps.Structures +{ + public interface IStartTime + { + public float StartTime { get; [MoonSharpVisible(false)] set; } + } +} diff --git a/Quaver.API/Maps/Structures/KeySoundInfo.cs b/Quaver.API/Maps/Structures/KeySoundInfo.cs index efa20a603..8f4f0d70c 100644 --- a/Quaver.API/Maps/Structures/KeySoundInfo.cs +++ b/Quaver.API/Maps/Structures/KeySoundInfo.cs @@ -1,3 +1,10 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * Copyright (c) 2017-2019 Swan & The Quaver Team . +*/ + using System; using System.Collections.Generic; @@ -30,6 +37,7 @@ public bool Equals(KeySoundInfo x, KeySoundInfo y) if (ReferenceEquals(x, null)) return false; if (ReferenceEquals(y, null)) return false; if (x.GetType() != y.GetType()) return false; + return x.Sample == y.Sample && x.Volume == y.Volume; } @@ -44,4 +52,4 @@ public int GetHashCode(KeySoundInfo obj) public static IEqualityComparer ByValueComparer { get; } = new ByValueEqualityComparer(); } -} \ No newline at end of file +} diff --git a/Quaver.API/Maps/Structures/SliderVelocityInfo.cs b/Quaver.API/Maps/Structures/SliderVelocityInfo.cs index d0cf70296..4037acd25 100644 --- a/Quaver.API/Maps/Structures/SliderVelocityInfo.cs +++ b/Quaver.API/Maps/Structures/SliderVelocityInfo.cs @@ -18,7 +18,7 @@ namespace Quaver.API.Maps.Structures /// [Serializable] [MoonSharpUserData] - public class SliderVelocityInfo + public class SliderVelocityInfo : IStartTime { /// /// The time in milliseconds when the new SliderVelocity section begins @@ -95,6 +95,7 @@ public bool Equals(SliderVelocityInfo x, SliderVelocityInfo y) if (ReferenceEquals(x, null)) return false; if (ReferenceEquals(y, null)) return false; if (x.GetType() != y.GetType()) return false; + return x.StartTime.Equals(y.StartTime) && x.Multiplier.Equals(y.Multiplier); } diff --git a/Quaver.API/Maps/Structures/SoundEffectInfo.cs b/Quaver.API/Maps/Structures/SoundEffectInfo.cs index 3ce5d6713..cd8bfa53e 100644 --- a/Quaver.API/Maps/Structures/SoundEffectInfo.cs +++ b/Quaver.API/Maps/Structures/SoundEffectInfo.cs @@ -14,7 +14,7 @@ namespace Quaver.API.Maps.Structures /// SoundEffects section of the .qua /// [Serializable] - public class SoundEffectInfo + public class SoundEffectInfo : IStartTime { /// /// The time at which to play the sound sample. @@ -42,6 +42,7 @@ public bool Equals(SoundEffectInfo x, SoundEffectInfo y) if (ReferenceEquals(x, null)) return false; if (ReferenceEquals(y, null)) return false; if (x.GetType() != y.GetType()) return false; + return x.StartTime.Equals(y.StartTime) && x.Sample == y.Sample && x.Volume == y.Volume; } @@ -59,4 +60,4 @@ public int GetHashCode(SoundEffectInfo obj) public static IEqualityComparer ByValueComparer { get; } = new ByValueEqualityComparer(); } -} \ No newline at end of file +} diff --git a/Quaver.API/Maps/Structures/TimingPointInfo.cs b/Quaver.API/Maps/Structures/TimingPointInfo.cs index cbfc3e198..c76b8a5aa 100644 --- a/Quaver.API/Maps/Structures/TimingPointInfo.cs +++ b/Quaver.API/Maps/Structures/TimingPointInfo.cs @@ -19,7 +19,7 @@ namespace Quaver.API.Maps.Structures /// [Serializable] [MoonSharpUserData] - public class TimingPointInfo + public class TimingPointInfo : IStartTime { /// /// The time in milliseconds for when this timing point begins @@ -86,6 +86,7 @@ public bool Equals(TimingPointInfo x, TimingPointInfo y) if (ReferenceEquals(x, null)) return false; if (ReferenceEquals(y, null)) return false; if (x.GetType() != y.GetType()) return false; + return x.StartTime.Equals(y.StartTime) && x.Bpm.Equals(y.Bpm) && x.Signature == y.Signature && x.Hidden == y.Hidden; } @@ -104,4 +105,4 @@ public int GetHashCode(TimingPointInfo obj) public static IEqualityComparer ByValueComparer { get; } = new ByValueEqualityComparer(); } -} \ No newline at end of file +} diff --git a/Quaver.API/Quaver.API.csproj b/Quaver.API/Quaver.API.csproj index 46ff69ebe..98668be6a 100644 --- a/Quaver.API/Quaver.API.csproj +++ b/Quaver.API/Quaver.API.csproj @@ -1,5 +1,6 @@  + true netstandard2.1 diff --git a/Quaver.API/Replays/ReplayFrame.cs b/Quaver.API/Replays/ReplayFrame.cs index 6a26c964e..5253b5a1e 100644 --- a/Quaver.API/Replays/ReplayFrame.cs +++ b/Quaver.API/Replays/ReplayFrame.cs @@ -1,19 +1,28 @@ /* * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * file, You can obtain one at http://mozilla.org/MPL/2.0/. * Copyright (c) 2017-2018 Swan & The Quaver Team . */ +using Quaver.API.Maps.Structures; + namespace Quaver.API.Replays { - public class ReplayFrame + public class ReplayFrame : IStartTime { /// /// The time in the replay since the last frame. /// public int Time { get; } + /// + float IStartTime.StartTime + { + get => Time; + set { } + } + /// /// The keys that were pressed during this frame. ///