From 062efed6cc3f2bcb99074a1307d23fe6fa217823 Mon Sep 17 00:00:00 2001 From: Emik Date: Fri, 26 Jul 2024 19:23:53 +0200 Subject: [PATCH 01/25] Optimize search algorithms --- Quaver.API/Helpers/StartTimeHelper.cs | 57 ++++ Quaver.API/Maps/Qua.cs | 167 +++------- Quaver.API/Maps/Structures/BookmarkInfo.cs | 17 +- Quaver.API/Maps/Structures/HitObjectInfo.cs | 288 +++++++++--------- Quaver.API/Maps/Structures/IStartTime.cs | 10 + .../Maps/Structures/SliderVelocityInfo.cs | 5 +- Quaver.API/Maps/Structures/SoundEffectInfo.cs | 7 +- Quaver.API/Maps/Structures/TimingPointInfo.cs | 7 +- Quaver.API/Replays/ReplayFrame.cs | 13 +- 9 files changed, 302 insertions(+), 269 deletions(-) create mode 100644 Quaver.API/Helpers/StartTimeHelper.cs create mode 100644 Quaver.API/Maps/Structures/IStartTime.cs diff --git a/Quaver.API/Helpers/StartTimeHelper.cs b/Quaver.API/Helpers/StartTimeHelper.cs new file mode 100644 index 000000000..294965166 --- /dev/null +++ b/Quaver.API/Helpers/StartTimeHelper.cs @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: MPL-2.0 +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using Quaver.API.Maps.Structures; + +namespace Quaver.API.Helpers +{ + public static class StartTimeHelper + { + public static int IndexAtTime(this IReadOnlyList 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; + } + + public static int IndexAtTimeBefore(this IReadOnlyList list, float time) + where T : IStartTime => + IndexAtTime(list, Before(time)); + + public static T AtTime(this IReadOnlyList list, float time) + where T : IStartTime + { + var i = list.IndexAtTime(time); + return i is -1 ? default : list[i]; + } + + public static T AtTimeBefore(this IReadOnlyList list, float time) + where T : IStartTime => + AtTime(list, Before(time)); + + // Thanks to https://stackoverflow.com/a/10426033 for the implementation. + 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; + + _ = float.IsNegative(time) ? Unsafe.As(ref time)-- : Unsafe.As(ref time)++; + return time; + } + + public static float Before(float time) => -After(-time); + } +} diff --git a/Quaver.API/Maps/Qua.cs b/Quaver.API/Maps/Qua.cs index 72f76286b..eb395efbf 100644 --- a/Quaver.API/Maps/Qua.cs +++ b/Quaver.API/Maps/Qua.cs @@ -11,16 +11,13 @@ 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; @@ -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 == 0 ? 0 : Math.Max(HitObjects[^1].StartTime, HitObjects[^1].EndTime); /// /// Integer based seed used for shuffling the lanes when randomize mod is active. @@ -447,11 +444,12 @@ 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(); + HitObjects.Sort(); + TimingPoints.Sort(); + SliderVelocities.Sort(); + SoundEffects.Sort(); + Bookmarks.Sort(); + SoundEffects.Sort(); } /// @@ -518,27 +516,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 +531,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 SortedDictionary(); - var durations = new Dictionary(); for (var i = TimingPoints.Count - 1; i >= 0; i--) { var point = TimingPoints[i]; @@ -565,19 +550,15 @@ public float GetCommonBpm() if (point.StartTime > lastTime) continue; - var duration = (int) (lastTime - (i == 0 ? 0 : point.StartTime)); + 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.Last().Key; } /// @@ -587,14 +568,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 +580,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. @@ -821,7 +791,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. @@ -979,33 +949,25 @@ 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 SortSliderVelocities() => SliderVelocities.Sort(); /// /// - public void SortTimingPoints() => TimingPoints = TimingPoints.OrderBy(x => x.StartTime).ToList(); + public void SortTimingPoints() => TimingPoints.Sort(); /// /// Gets the judgement of a particular hitobject in the map @@ -1014,22 +976,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 +996,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; - - if (total - 1 == index) - { - h = HitObjects[i]; - break; - } - - if (HitObjects[i].IsLongNote) - total += 1; - - if (total - 1 == index) - { - h = HitObjects[i]; - break; - } - } + // ReSharper disable once ForeachCanBeConvertedToQueryUsingAnotherGetEnumerator + foreach (var h in HitObjects) + if (total++ == index || h.IsLongNote && total++ == index) + return h; - return h; + return null; } /// @@ -1407,24 +1347,15 @@ 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; - - return $"{Path.GetDirectoryName(FilePath)}/{AudioFile}"; - } + public string GetAudioPath() => GetFullPath(AudioFile); } } diff --git a/Quaver.API/Maps/Structures/BookmarkInfo.cs b/Quaver.API/Maps/Structures/BookmarkInfo.cs index 6507e9d13..bc7974bf2 100644 --- a/Quaver.API/Maps/Structures/BookmarkInfo.cs +++ b/Quaver.API/Maps/Structures/BookmarkInfo.cs @@ -7,20 +7,29 @@ namespace Quaver.API.Maps.Structures { [MoonSharpUserData] [Serializable] - public class BookmarkInfo + public class BookmarkInfo : IComparable, IStartTime { public int StartTime { - get; + get; [MoonSharpVisible(false)] set; } public string Note { - get; + get; [MoonSharpVisible(false)] set; } + float IStartTime.StartTime + { + get => StartTime; + set => StartTime = (int)value; + } + + /// + public int CompareTo(BookmarkInfo other) => StartTime.CompareTo(other.StartTime); + private sealed class TimeNoteEqualityComparer : IEqualityComparer { public bool Equals(BookmarkInfo x, BookmarkInfo y) @@ -40,4 +49,4 @@ public int GetHashCode(BookmarkInfo obj) public static IEqualityComparer ByValueComparer { get; } = new TimeNoteEqualityComparer(); } -} \ No newline at end of file +} diff --git a/Quaver.API/Maps/Structures/HitObjectInfo.cs b/Quaver.API/Maps/Structures/HitObjectInfo.cs index 61ce6e234..36ddf3f25 100644 --- a/Quaver.API/Maps/Structures/HitObjectInfo.cs +++ b/Quaver.API/Maps/Structures/HitObjectInfo.cs @@ -11,177 +11,185 @@ 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 : IComparable, IStartTime + { - /// - /// The time in milliseconds when the HitObject is supposed to be hit. - /// - public int StartTime - { - get; - [MoonSharpVisible(false)] set; - } + /// + /// The time in milliseconds when the HitObject is supposed to be hit. + /// + public int StartTime + { + get; + [MoonSharpVisible(false)] set; + } - /// - /// The lane the HitObject falls in - /// - public int Lane - { - get; - [MoonSharpVisible(false)] set; - } + /// + /// The lane the HitObject falls in + /// + public int Lane + { + get; + [MoonSharpVisible(false)] set; + } - /// - /// The endtime of the HitObject (if greater than 0, it's considered a hold note.) - /// - public int EndTime - { - get; - [MoonSharpVisible(false)] set; - } + /// + /// The endtime of the HitObject (if greater than 0, it's considered a hold note.) + /// + public int EndTime + { + get; + [MoonSharpVisible(false)] set; + } - /// - /// Bitwise combination of hit sounds for this object - /// - public HitSounds HitSound - { - get; - [MoonSharpVisible(false)] set; - } + float IStartTime.StartTime + { + get => StartTime; + set => StartTime = (int)value; + } - /// - /// Key sounds to play when this object is hit. - /// - [MoonSharpVisible(false)] - public List KeySounds { get; set; } = new List(); + /// + /// Bitwise combination of hit sounds for this object + /// + public HitSounds HitSound + { + get; + [MoonSharpVisible(false)] set; + } - /// - /// The layer in the editor that the object belongs to. - /// - public int EditorLayer - { - get; - [MoonSharpVisible(false)] set; - } + /// + /// Key sounds to play when this object is hit. + /// + [MoonSharpVisible(false)] + public List KeySounds { get; set; } = new List(); - /// - /// If the object is a long note. (EndTime > 0) - /// - [YamlIgnore] - public bool IsLongNote => EndTime > 0; - - /// - /// Returns if the object is allowed to be edited in lua scripts - /// - [YamlIgnore] - public bool IsEditableInLuaScript - { - get; - [MoonSharpVisible(false)] set; - } + /// + /// The layer in the editor that the object belongs to. + /// + public int EditorLayer + { + get; + [MoonSharpVisible(false)] set; + } - /// - /// 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]; - } + /// + /// If the object is a long note. (EndTime > 0) + /// + [YamlIgnore] + public bool IsLongNote => EndTime > 0; - return timingPoints.First(); - } + /// + /// Returns if the object is allowed to be edited in lua scripts + /// + [YamlIgnore] + public bool IsEditableInLuaScript + { + get; + [MoonSharpVisible(false)] set; + } - /// - /// - /// - /// - public void SetStartTime(int time) - { - ThrowUneditableException(); - StartTime = time; - } + /// + public int CompareTo(HitObjectInfo other) => StartTime.CompareTo(other.StartTime); - /// - /// - /// - public void SetEndTime(int time) - { - ThrowUneditableException(); - EndTime = time; - } + /// + /// Gets the timing point this object is in range of. + /// + /// + public TimingPointInfo GetTimingPoint(List timingPoints) => timingPoints.AtTime(StartTime); - /// - /// - /// - public void SetLane(int lane) - { - ThrowUneditableException(); - Lane = lane; - } + /// + /// + /// + /// + public void SetStartTime(int time) + { + ThrowUneditableException(); + StartTime = time; + } - /// - /// - /// - public void SetHitSounds(HitSounds hitsounds) - { - ThrowUneditableException(); - HitSound = hitsounds; - } + /// + /// + /// + public void SetEndTime(int time) + { + ThrowUneditableException(); + EndTime = time; + } - /// - /// - /// - private void ThrowUneditableException() + /// + /// + /// + public void SetLane(int lane) + { + ThrowUneditableException(); + Lane = lane; + } + + /// + /// + /// + public void SetHitSounds(HitSounds hitsounds) + { + ThrowUneditableException(); + HitSound = hitsounds; + } + + /// + /// + /// + private void ThrowUneditableException() + { + if (!IsEditableInLuaScript) + throw new InvalidOperationException("Value is not allowed to be edited in lua scripts."); + } + + /// + /// By-value comparer, mostly auto-generated by Rider: KeySounds-related code is changed to by-value. + /// + private sealed class ByValueEqualityComparer : IEqualityComparer + { + public bool Equals(HitObjectInfo x, HitObjectInfo y) { - if (!IsEditableInLuaScript) - throw new InvalidOperationException("Value is not allowed to be edited in lua scripts."); + if (ReferenceEquals(x, y)) return true; + 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; } - /// - /// By-value comparer, mostly auto-generated by Rider: KeySounds-related code is changed to by-value. - /// - private sealed class ByValueEqualityComparer : IEqualityComparer + public int GetHashCode(HitObjectInfo obj) { - public bool Equals(HitObjectInfo x, HitObjectInfo y) + unchecked { - if (ReferenceEquals(x, y)) return true; - 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; - } + var hashCode = obj.StartTime; + hashCode = (hashCode * 397) ^ obj.Lane; + hashCode = (hashCode * 397) ^ obj.EndTime; + hashCode = (hashCode * 397) ^ (int)obj.HitSound; - public int GetHashCode(HitObjectInfo obj) - { - unchecked - { - var hashCode = obj.StartTime; - 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; - } + foreach (var keySound in obj.KeySounds) + hashCode = (hashCode * 397) ^ KeySoundInfo.ByValueComparer.GetHashCode(keySound); + + hashCode = (hashCode * 397) ^ obj.EditorLayer; + return hashCode; } } + } - public static IEqualityComparer ByValueComparer { get; } = new ByValueEqualityComparer(); + public static IEqualityComparer ByValueComparer { get; } = new ByValueEqualityComparer(); } } diff --git a/Quaver.API/Maps/Structures/IStartTime.cs b/Quaver.API/Maps/Structures/IStartTime.cs new file mode 100644 index 000000000..e562eab73 --- /dev/null +++ b/Quaver.API/Maps/Structures/IStartTime.cs @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: MPL-2.0 +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/SliderVelocityInfo.cs b/Quaver.API/Maps/Structures/SliderVelocityInfo.cs index ff3153851..855d22d68 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 : IComparable, IStartTime { /// /// The time in milliseconds when the new SliderVelocity section begins @@ -81,6 +81,9 @@ private void ThrowUneditableException() throw new InvalidOperationException("Value is not allowed to be edited in lua scripts."); } + /// + public int CompareTo(SliderVelocityInfo other) => StartTime.CompareTo(other.StartTime); + /// /// By-value comparer, auto-generated by Rider. /// diff --git a/Quaver.API/Maps/Structures/SoundEffectInfo.cs b/Quaver.API/Maps/Structures/SoundEffectInfo.cs index 3ce5d6713..1830b65be 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 : IComparable, IStartTime { /// /// The time at which to play the sound sample. @@ -31,6 +31,9 @@ public class SoundEffectInfo /// public int Volume { get; set; } + /// + public int CompareTo(SoundEffectInfo other) => StartTime.CompareTo(other.StartTime); + /// /// By-value comparer, auto-generated by Rider. /// @@ -59,4 +62,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 492a2ae7c..c33ac4277 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 : IComparable, IStartTime { /// /// The time in milliseconds for when this timing point begins @@ -70,6 +70,9 @@ public bool IsEditableInLuaScript [YamlIgnore] public float MillisecondsPerBeat => 60000 / Bpm; + /// + public int CompareTo(TimingPointInfo other) => StartTime.CompareTo(other.StartTime); + /// /// By-value comparer, auto-generated by Rider. /// @@ -99,4 +102,4 @@ public int GetHashCode(TimingPointInfo obj) public static IEqualityComparer ByValueComparer { get; } = new ByValueEqualityComparer(); } -} \ No newline at end of file +} diff --git a/Quaver.API/Replays/ReplayFrame.cs b/Quaver.API/Replays/ReplayFrame.cs index 0678c44f1..0094f28b5 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. /// From 96a10a1389275dc6b0f911650b49537a3ad99eed Mon Sep 17 00:00:00 2001 From: Emik Date: Fri, 26 Jul 2024 19:47:02 +0200 Subject: [PATCH 02/25] Fix GetCommonBpm --- Quaver.API/Maps/Qua.cs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/Quaver.API/Maps/Qua.cs b/Quaver.API/Maps/Qua.cs index eb395efbf..2f1eafae1 100644 --- a/Quaver.API/Maps/Qua.cs +++ b/Quaver.API/Maps/Qua.cs @@ -540,7 +540,7 @@ public float GetCommonBpm() var lastObject = HitObjects.OrderByDescending(x => x.IsLongNote ? x.EndTime : x.StartTime).First(); double lastTime = lastObject.IsLongNote ? lastObject.EndTime : lastObject.StartTime; - var durations = new SortedDictionary(); + var durations = new Dictionary(); for (var i = TimingPoints.Count - 1; i >= 0; i--) { @@ -557,8 +557,16 @@ public float GetCommonBpm() durations[point.Bpm] += duration; } - // osu! hangs on loading the map in this case; we return a sensible result. - return durations.Count is 0 ? TimingPoints[0].Bpm : durations.Last().Key; + if (durations.Count is 0) // osu! hangs on loading the map in this case; we return a sensible result. + return TimingPoints[0].Bpm; + + var max = (Bpm: 0f, Duration: 0); + + foreach (var (bpm, duration) in durations) + if (duration > max.Duration) + max = (bpm, duration); + + return max.Bpm; } /// From 9e5815a90b844fb6d459a23bad06de16130a3f76 Mon Sep 17 00:00:00 2001 From: Emik Date: Fri, 26 Jul 2024 19:47:12 +0200 Subject: [PATCH 03/25] Use unsafe code instead of Unsafe.As --- Quaver.API/Helpers/StartTimeHelper.cs | 7 ++++++- Quaver.API/Quaver.API.csproj | 1 + 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/Quaver.API/Helpers/StartTimeHelper.cs b/Quaver.API/Helpers/StartTimeHelper.cs index 294965166..93218b9dd 100644 --- a/Quaver.API/Helpers/StartTimeHelper.cs +++ b/Quaver.API/Helpers/StartTimeHelper.cs @@ -48,7 +48,12 @@ public static float After(float time) if (time is 0) return float.Epsilon; - _ = float.IsNegative(time) ? Unsafe.As(ref time)-- : Unsafe.As(ref time)++; + unsafe + { + // Slightly evil bit hack. + _ = time > 0 ? ++*(int*)&time : --*(int*)&time; + } + return time; } 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 From e4a18459903f315f547dade4a27ffa113ae23c75 Mon Sep 17 00:00:00 2001 From: Emik Date: Fri, 26 Jul 2024 19:56:08 +0200 Subject: [PATCH 04/25] Make style changes --- Quaver.API/Helpers/StartTimeHelper.cs | 2 +- Quaver.API/Maps/Structures/HitObjectInfo.cs | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/Quaver.API/Helpers/StartTimeHelper.cs b/Quaver.API/Helpers/StartTimeHelper.cs index 93218b9dd..181a1be48 100644 --- a/Quaver.API/Helpers/StartTimeHelper.cs +++ b/Quaver.API/Helpers/StartTimeHelper.cs @@ -14,7 +14,7 @@ public static int IndexAtTime(this IReadOnlyList list, float time) var right = list.Count - 1; while (left <= right) - if (left + (right - left) / 2 is var mid && list[mid].StartTime <= time) + if ((left + (right - left) / 2) is var mid && list[mid].StartTime <= time) left = mid + 1; else right = mid - 1; diff --git a/Quaver.API/Maps/Structures/HitObjectInfo.cs b/Quaver.API/Maps/Structures/HitObjectInfo.cs index 36ddf3f25..0d18c4a54 100644 --- a/Quaver.API/Maps/Structures/HitObjectInfo.cs +++ b/Quaver.API/Maps/Structures/HitObjectInfo.cs @@ -22,7 +22,6 @@ namespace Quaver.API.Maps.Structures [MoonSharpUserData] [Serializable] public class HitObjectInfo : IComparable, IStartTime - { /// /// The time in milliseconds when the HitObject is supposed to be hit. From bef3dbc365bba20c3501e4464be3321cf24fa925 Mon Sep 17 00:00:00 2001 From: Emik Date: Fri, 26 Jul 2024 19:56:26 +0200 Subject: [PATCH 05/25] Revert GetCommonBpm --- Quaver.API/Maps/Qua.cs | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/Quaver.API/Maps/Qua.cs b/Quaver.API/Maps/Qua.cs index 2f1eafae1..83d7b02fd 100644 --- a/Quaver.API/Maps/Qua.cs +++ b/Quaver.API/Maps/Qua.cs @@ -560,13 +560,7 @@ public float GetCommonBpm() if (durations.Count is 0) // osu! hangs on loading the map in this case; we return a sensible result. return TimingPoints[0].Bpm; - var max = (Bpm: 0f, Duration: 0); - - foreach (var (bpm, duration) in durations) - if (duration > max.Duration) - max = (bpm, duration); - - return max.Bpm; + return durations.OrderByDescending(x => x.Value).First().Key; } /// @@ -1008,7 +1002,7 @@ public HitObjectInfo GetHitObjectAtJudgementIndex(int index) // ReSharper disable once ForeachCanBeConvertedToQueryUsingAnotherGetEnumerator foreach (var h in HitObjects) - if (total++ == index || h.IsLongNote && total++ == index) + if (total++ == index || (h.IsLongNote && total++ == index)) return h; return null; From c89ab86f01a3ce138f64cd4fb0d11c3743a61182 Mon Sep 17 00:00:00 2001 From: Emik Date: Sat, 27 Jul 2024 01:03:17 +0200 Subject: [PATCH 06/25] Sort SVs after denormalizing/normalizing --- Quaver.API/Helpers/StartTimeHelper.cs | 2 +- Quaver.API/Maps/Qua.cs | 22 ++++++++-------------- 2 files changed, 9 insertions(+), 15 deletions(-) diff --git a/Quaver.API/Helpers/StartTimeHelper.cs b/Quaver.API/Helpers/StartTimeHelper.cs index 181a1be48..90e33c9f6 100644 --- a/Quaver.API/Helpers/StartTimeHelper.cs +++ b/Quaver.API/Helpers/StartTimeHelper.cs @@ -14,7 +14,7 @@ public static int IndexAtTime(this IReadOnlyList list, float time) var right = list.Count - 1; while (left <= right) - if ((left + (right - left) / 2) is var mid && list[mid].StartTime <= time) + if ((left + ((right - left) / 2)) is var mid && list[mid].StartTime <= time) left = mid + 1; else right = mid - 1; diff --git a/Quaver.API/Maps/Qua.cs b/Quaver.API/Maps/Qua.cs index 83d7b02fd..87628a6c7 100644 --- a/Quaver.API/Maps/Qua.cs +++ b/Quaver.API/Maps/Qua.cs @@ -707,12 +707,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; - } } /// @@ -890,7 +886,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.Sort(); + HitObjects = newHitObjects; } /// @@ -930,18 +927,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; - } } /// @@ -1176,9 +1168,10 @@ public void NormalizeSVs() } } + normalizedScrollVelocities.Sort(); BPMDoesNotAffectScrollVelocity = true; - InitialScrollVelocity = initialSvMultiplier ?? 1; SliderVelocities = normalizedScrollVelocities; + InitialScrollVelocity = initialSvMultiplier ?? 1; } /// @@ -1308,8 +1301,9 @@ public void DenormalizeSVs() } } - BPMDoesNotAffectScrollVelocity = false; InitialScrollVelocity = 0; + denormalizedScrollVelocities.Sort(); + BPMDoesNotAffectScrollVelocity = false; SliderVelocities = denormalizedScrollVelocities; } From ea8ceb30286f5de9648ead8ae09f98ace23b5c94 Mon Sep 17 00:00:00 2001 From: Emik Date: Sat, 27 Jul 2024 01:23:14 +0200 Subject: [PATCH 07/25] Clone lists just in case they somehow get mutated by other copies --- Quaver.API/Maps/Qua.cs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/Quaver.API/Maps/Qua.cs b/Quaver.API/Maps/Qua.cs index 87628a6c7..103ad6c69 100644 --- a/Quaver.API/Maps/Qua.cs +++ b/Quaver.API/Maps/Qua.cs @@ -444,12 +444,16 @@ public List Validate() /// public void Sort() { + Bookmarks = new List(Bookmarks); + HitObjects = new List(HitObjects); + SoundEffects = new List(SoundEffects); + TimingPoints = new List(TimingPoints); + SliderVelocities = new List(SliderVelocities); + Bookmarks.Sort(); HitObjects.Sort(); + SoundEffects.Sort(); TimingPoints.Sort(); SliderVelocities.Sort(); - SoundEffects.Sort(); - Bookmarks.Sort(); - SoundEffects.Sort(); } /// From 1b49e126e883bf9ec2780e7943b2ce1b31a1a8c1 Mon Sep 17 00:00:00 2001 From: Emik Date: Sat, 27 Jul 2024 01:48:04 +0200 Subject: [PATCH 08/25] Improve test error messages --- Quaver.API.Tests/Quaver/TestCaseQua.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Quaver.API.Tests/Quaver/TestCaseQua.cs b/Quaver.API.Tests/Quaver/TestCaseQua.cs index 1d05f332d..c58dd8165 100644 --- a/Quaver.API.Tests/Quaver/TestCaseQua.cs +++ b/Quaver.API.Tests/Quaver/TestCaseQua.cs @@ -282,22 +282,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 +} From 08712b9054bba8a18e60ef4b44f67077bf9b336d Mon Sep 17 00:00:00 2001 From: Emik Date: Sat, 27 Jul 2024 01:51:01 +0200 Subject: [PATCH 09/25] Make sorting fully deterministic --- Quaver.API/Maps/Qua.cs | 5 --- Quaver.API/Maps/Structures/BookmarkInfo.cs | 12 ++++++- Quaver.API/Maps/Structures/HitObjectInfo.cs | 31 +++++++++++++++++-- .../Maps/Structures/SliderVelocityInfo.cs | 16 ++++++++-- Quaver.API/Maps/Structures/SoundEffectInfo.cs | 17 +++++++++- Quaver.API/Maps/Structures/TimingPointInfo.cs | 22 ++++++++++++- 6 files changed, 89 insertions(+), 14 deletions(-) diff --git a/Quaver.API/Maps/Qua.cs b/Quaver.API/Maps/Qua.cs index 103ad6c69..8468da709 100644 --- a/Quaver.API/Maps/Qua.cs +++ b/Quaver.API/Maps/Qua.cs @@ -444,11 +444,6 @@ public List Validate() /// public void Sort() { - Bookmarks = new List(Bookmarks); - HitObjects = new List(HitObjects); - SoundEffects = new List(SoundEffects); - TimingPoints = new List(TimingPoints); - SliderVelocities = new List(SliderVelocities); Bookmarks.Sort(); HitObjects.Sort(); SoundEffects.Sort(); diff --git a/Quaver.API/Maps/Structures/BookmarkInfo.cs b/Quaver.API/Maps/Structures/BookmarkInfo.cs index bc7974bf2..4f8f6c585 100644 --- a/Quaver.API/Maps/Structures/BookmarkInfo.cs +++ b/Quaver.API/Maps/Structures/BookmarkInfo.cs @@ -28,7 +28,17 @@ float IStartTime.StartTime } /// - public int CompareTo(BookmarkInfo other) => StartTime.CompareTo(other.StartTime); + public int CompareTo(BookmarkInfo other) + { + if (ReferenceEquals(this, other)) + return 0; + + if (other is null) + return 1; + + var compare = StartTime.CompareTo(other.StartTime); + return compare is 0 ? string.Compare(Note, other.Note, StringComparison.Ordinal) : compare; + } private sealed class TimeNoteEqualityComparer : IEqualityComparer { diff --git a/Quaver.API/Maps/Structures/HitObjectInfo.cs b/Quaver.API/Maps/Structures/HitObjectInfo.cs index 0d18c4a54..c5276ca65 100644 --- a/Quaver.API/Maps/Structures/HitObjectInfo.cs +++ b/Quaver.API/Maps/Structures/HitObjectInfo.cs @@ -96,9 +96,6 @@ public bool IsEditableInLuaScript [MoonSharpVisible(false)] set; } - /// - public int CompareTo(HitObjectInfo other) => StartTime.CompareTo(other.StartTime); - /// /// Gets the timing point this object is in range of. /// @@ -151,6 +148,34 @@ private void ThrowUneditableException() throw new InvalidOperationException("Value is not allowed to be edited in lua scripts."); } + /// + public int CompareTo(HitObjectInfo other) + { + if (ReferenceEquals(this, other)) + return 0; + + if (other is null) + return 1; + + var compare = StartTime.CompareTo(other.StartTime); + + if (compare != 0) + return compare; + + compare = Lane.CompareTo(other.Lane); + + if (compare != 0) + return compare; + + compare = EndTime.CompareTo(other.EndTime); + + if (compare != 0) + return compare; + + compare = HitSound.CompareTo(other.HitSound); + return compare is 0 ? EditorLayer.CompareTo(other.EditorLayer) : compare; + } + /// /// By-value comparer, mostly auto-generated by Rider: KeySounds-related code is changed to by-value. /// diff --git a/Quaver.API/Maps/Structures/SliderVelocityInfo.cs b/Quaver.API/Maps/Structures/SliderVelocityInfo.cs index 855d22d68..94958376e 100644 --- a/Quaver.API/Maps/Structures/SliderVelocityInfo.cs +++ b/Quaver.API/Maps/Structures/SliderVelocityInfo.cs @@ -72,6 +72,19 @@ public void SetMultiplier(float multiplier) Multiplier = multiplier; } + /// + public int CompareTo(SliderVelocityInfo other) + { + if (ReferenceEquals(this, other)) + return 0; + + if (other is null) + return 1; + + var compare = StartTime.CompareTo(other.StartTime); + return compare is 0 ? Multiplier.CompareTo(other.Multiplier) : compare; + } + /// /// /// @@ -81,9 +94,6 @@ private void ThrowUneditableException() throw new InvalidOperationException("Value is not allowed to be edited in lua scripts."); } - /// - public int CompareTo(SliderVelocityInfo other) => StartTime.CompareTo(other.StartTime); - /// /// By-value comparer, auto-generated by Rider. /// diff --git a/Quaver.API/Maps/Structures/SoundEffectInfo.cs b/Quaver.API/Maps/Structures/SoundEffectInfo.cs index 1830b65be..afaa79c25 100644 --- a/Quaver.API/Maps/Structures/SoundEffectInfo.cs +++ b/Quaver.API/Maps/Structures/SoundEffectInfo.cs @@ -32,7 +32,22 @@ public class SoundEffectInfo : IComparable, IStartTime public int Volume { get; set; } /// - public int CompareTo(SoundEffectInfo other) => StartTime.CompareTo(other.StartTime); + public int CompareTo(SoundEffectInfo other) + { + if (ReferenceEquals(this, other)) + return 0; + + if (other is null) + return 1; + + var compare = StartTime.CompareTo(other.StartTime); + + if (compare != 0) + return compare; + + compare = Sample.CompareTo(other.Sample); + return compare is 0 ? Volume.CompareTo(other.Volume) : compare; + } /// /// By-value comparer, auto-generated by Rider. diff --git a/Quaver.API/Maps/Structures/TimingPointInfo.cs b/Quaver.API/Maps/Structures/TimingPointInfo.cs index c33ac4277..8d3c8eb07 100644 --- a/Quaver.API/Maps/Structures/TimingPointInfo.cs +++ b/Quaver.API/Maps/Structures/TimingPointInfo.cs @@ -71,7 +71,27 @@ public bool IsEditableInLuaScript public float MillisecondsPerBeat => 60000 / Bpm; /// - public int CompareTo(TimingPointInfo other) => StartTime.CompareTo(other.StartTime); + public int CompareTo(TimingPointInfo other) + { + if (ReferenceEquals(this, other)) + return 0; + + if (other is null) + return 1; + + var compare = StartTime.CompareTo(other.StartTime); + + if (compare != 0) + return compare; + + compare = Bpm.CompareTo(other.Bpm); + + if (compare != 0) + return compare; + + compare = Signature.CompareTo(other.Signature); + return compare is 0 ? Hidden.CompareTo(other.Hidden) : compare; + } /// /// By-value comparer, auto-generated by Rider. From 70a99b54366df82a17780c9382080be0d5a73b3f Mon Sep 17 00:00:00 2001 From: Emik Date: Sat, 27 Jul 2024 01:54:17 +0200 Subject: [PATCH 10/25] Fix edgecase brought up by @WilliamQiufeng --- Quaver.API/Maps/Qua.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Quaver.API/Maps/Qua.cs b/Quaver.API/Maps/Qua.cs index 8468da709..2ad46a056 100644 --- a/Quaver.API/Maps/Qua.cs +++ b/Quaver.API/Maps/Qua.cs @@ -173,7 +173,8 @@ public class Qua /// /// [YamlIgnore] - public int Length => HitObjects.Count == 0 ? 0 : Math.Max(HitObjects[^1].StartTime, HitObjects[^1].EndTime); + public int Length => + HitObjects.Count is 0 ? 0 : Math.Max(HitObjects[^1].StartTime, HitObjects.Max(x => x.EndTime)); /// /// Integer based seed used for shuffling the lanes when randomize mod is active. From d458378cc04c27215761a712d9db49db1c6374a4 Mon Sep 17 00:00:00 2001 From: Emik Date: Sat, 27 Jul 2024 02:29:58 +0200 Subject: [PATCH 11/25] Add InsertSorted, which will be used to replace Add -> Sort (slow!) --- Quaver.API/Helpers/StartTimeHelper.cs | 31 +++++++++++++++++++++------ 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/Quaver.API/Helpers/StartTimeHelper.cs b/Quaver.API/Helpers/StartTimeHelper.cs index 90e33c9f6..0b246e7ea 100644 --- a/Quaver.API/Helpers/StartTimeHelper.cs +++ b/Quaver.API/Helpers/StartTimeHelper.cs @@ -1,20 +1,39 @@ // SPDX-License-Identifier: MPL-2.0 +using System; using System.Collections.Generic; -using System.Runtime.CompilerServices; using Quaver.API.Maps.Structures; namespace Quaver.API.Helpers { public static class StartTimeHelper { - public static int IndexAtTime(this IReadOnlyList list, float time) + public static void InsertSorted(this List list, T element) + where T : IComparable + { + var i = list.BinarySearch(element); + list.Insert(i >= 0 ? i : ~i, element); + } + + public static void InsertSorted(this List list, ICollection elements) + where T : IComparable + { + if (list.Capacity - list.Count < elements.Count) + list.Capacity = Math.Max(list.Capacity * 2, list.Capacity + elements.Count); + + foreach (var element in elements) + InsertSorted(list, element); + } + + // Ideally would be IReadOnlyList to indicate no mutation, + // but unfortunately IList doesn't implement IReadOnlyList. + public static int IndexAtTime(this IList 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) + if (left + ((right - left) / 2) is var mid && list[mid].StartTime <= time) left = mid + 1; else right = mid - 1; @@ -22,18 +41,18 @@ public static int IndexAtTime(this IReadOnlyList list, float time) return right; } - public static int IndexAtTimeBefore(this IReadOnlyList list, float time) + public static int IndexAtTimeBefore(this IList list, float time) where T : IStartTime => IndexAtTime(list, Before(time)); - public static T AtTime(this IReadOnlyList list, float time) + public static T AtTime(this IList list, float time) where T : IStartTime { var i = list.IndexAtTime(time); return i is -1 ? default : list[i]; } - public static T AtTimeBefore(this IReadOnlyList list, float time) + public static T AtTimeBefore(this IList list, float time) where T : IStartTime => AtTime(list, Before(time)); From 34dfca20812b009c4eae84f05a6619ac97127a9a Mon Sep 17 00:00:00 2001 From: Emik Date: Sat, 27 Jul 2024 02:58:30 +0200 Subject: [PATCH 12/25] Add missing methods --- Quaver.API/Helpers/StartTimeHelper.cs | 2 +- Quaver.API/Maps/Qua.cs | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/Quaver.API/Helpers/StartTimeHelper.cs b/Quaver.API/Helpers/StartTimeHelper.cs index 0b246e7ea..ecd17d598 100644 --- a/Quaver.API/Helpers/StartTimeHelper.cs +++ b/Quaver.API/Helpers/StartTimeHelper.cs @@ -14,7 +14,7 @@ public static void InsertSorted(this List list, T element) list.Insert(i >= 0 ? i : ~i, element); } - public static void InsertSorted(this List list, ICollection elements) + public static void InsertSorted(this List list, IReadOnlyCollection elements) where T : IComparable { if (list.Capacity - list.Count < elements.Count) diff --git a/Quaver.API/Maps/Qua.cs b/Quaver.API/Maps/Qua.cs index 2ad46a056..3cb6b4f40 100644 --- a/Quaver.API/Maps/Qua.cs +++ b/Quaver.API/Maps/Qua.cs @@ -955,10 +955,22 @@ public void MirrorHitObjects() temp.Lane = keyCount - temp.Lane + 1; } + /// + /// + public void SortBookmarks() => Bookmarks.Sort(); + + /// + /// + public void SortHitObjects() => HitObjects.Sort(); + /// /// public void SortSliderVelocities() => SliderVelocities.Sort(); + /// + /// + public void SortSoundEffects() => SoundEffects.Sort(); + /// /// public void SortTimingPoints() => TimingPoints.Sort(); From 3ad98f1d58f87bf1224e6f238a7b80cea3fb69e4 Mon Sep 17 00:00:00 2001 From: Emik Date: Sat, 27 Jul 2024 23:39:25 +0200 Subject: [PATCH 13/25] Use hybrid insertion algorithm --- Quaver.API/Helpers/StartTimeHelper.cs | 69 +++++++++++++++++++++++++-- 1 file changed, 64 insertions(+), 5 deletions(-) diff --git a/Quaver.API/Helpers/StartTimeHelper.cs b/Quaver.API/Helpers/StartTimeHelper.cs index ecd17d598..11b8b1070 100644 --- a/Quaver.API/Helpers/StartTimeHelper.cs +++ b/Quaver.API/Helpers/StartTimeHelper.cs @@ -1,6 +1,9 @@ // SPDX-License-Identifier: MPL-2.0 using System; +using System.Collections; using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; using Quaver.API.Maps.Structures; namespace Quaver.API.Helpers @@ -14,14 +17,35 @@ public static void InsertSorted(this List list, T element) list.Insert(i >= 0 ? i : ~i, element); } - public static void InsertSorted(this List list, IReadOnlyCollection elements) + public static void InsertSorted(this List list, IEnumerable elements) where T : IComparable { - if (list.Capacity - list.Count < elements.Count) - list.Capacity = Math.Max(list.Capacity * 2, list.Capacity + elements.Count); + // 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; - foreach (var element in elements) - InsertSorted(list, element); + 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.Sort(); + break; + } } // Ideally would be IReadOnlyList to indicate no mutation, @@ -77,5 +101,40 @@ public static float After(float time) } 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 : IComparable + { + 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; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + 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, + }; } } From 823d67614864a4233d481df126856f36ba8269d4 Mon Sep 17 00:00:00 2001 From: Emik Date: Sat, 27 Jul 2024 23:51:04 +0200 Subject: [PATCH 14/25] Fix performance regression --- Quaver.API/Helpers/StartTimeHelper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Quaver.API/Helpers/StartTimeHelper.cs b/Quaver.API/Helpers/StartTimeHelper.cs index 11b8b1070..f6a472ae1 100644 --- a/Quaver.API/Helpers/StartTimeHelper.cs +++ b/Quaver.API/Helpers/StartTimeHelper.cs @@ -33,7 +33,7 @@ public static void InsertSorted(this List list, IEnumerable elements) case 1: InsertSorted(list, elements.First()); break; - case { } count when count <= MinimumCount && list.Capacity <= MaximumCapacity: + case { } count when count <= MinimumCount && list.Capacity >= MaximumCapacity: var capacity = list.Capacity; if (capacity - list.Count < count) From d3d4e8b59060f2b82bf734b421512a47dd5a4265 Mon Sep 17 00:00:00 2001 From: Emik Date: Tue, 30 Jul 2024 05:34:33 +0200 Subject: [PATCH 15/25] Revert deterministic sorting, except for HitObjects --- Quaver.API/Maps/Structures/BookmarkInfo.cs | 12 +--------- .../Maps/Structures/SliderVelocityInfo.cs | 12 +--------- Quaver.API/Maps/Structures/SoundEffectInfo.cs | 17 +------------- Quaver.API/Maps/Structures/TimingPointInfo.cs | 22 +------------------ 4 files changed, 4 insertions(+), 59 deletions(-) diff --git a/Quaver.API/Maps/Structures/BookmarkInfo.cs b/Quaver.API/Maps/Structures/BookmarkInfo.cs index 4f8f6c585..bc7974bf2 100644 --- a/Quaver.API/Maps/Structures/BookmarkInfo.cs +++ b/Quaver.API/Maps/Structures/BookmarkInfo.cs @@ -28,17 +28,7 @@ float IStartTime.StartTime } /// - public int CompareTo(BookmarkInfo other) - { - if (ReferenceEquals(this, other)) - return 0; - - if (other is null) - return 1; - - var compare = StartTime.CompareTo(other.StartTime); - return compare is 0 ? string.Compare(Note, other.Note, StringComparison.Ordinal) : compare; - } + public int CompareTo(BookmarkInfo other) => StartTime.CompareTo(other.StartTime); private sealed class TimeNoteEqualityComparer : IEqualityComparer { diff --git a/Quaver.API/Maps/Structures/SliderVelocityInfo.cs b/Quaver.API/Maps/Structures/SliderVelocityInfo.cs index 94958376e..0caed7e61 100644 --- a/Quaver.API/Maps/Structures/SliderVelocityInfo.cs +++ b/Quaver.API/Maps/Structures/SliderVelocityInfo.cs @@ -73,17 +73,7 @@ public void SetMultiplier(float multiplier) } /// - public int CompareTo(SliderVelocityInfo other) - { - if (ReferenceEquals(this, other)) - return 0; - - if (other is null) - return 1; - - var compare = StartTime.CompareTo(other.StartTime); - return compare is 0 ? Multiplier.CompareTo(other.Multiplier) : compare; - } + public int CompareTo(SliderVelocityInfo other) => StartTime.CompareTo(other.StartTime); /// /// diff --git a/Quaver.API/Maps/Structures/SoundEffectInfo.cs b/Quaver.API/Maps/Structures/SoundEffectInfo.cs index afaa79c25..1830b65be 100644 --- a/Quaver.API/Maps/Structures/SoundEffectInfo.cs +++ b/Quaver.API/Maps/Structures/SoundEffectInfo.cs @@ -32,22 +32,7 @@ public class SoundEffectInfo : IComparable, IStartTime public int Volume { get; set; } /// - public int CompareTo(SoundEffectInfo other) - { - if (ReferenceEquals(this, other)) - return 0; - - if (other is null) - return 1; - - var compare = StartTime.CompareTo(other.StartTime); - - if (compare != 0) - return compare; - - compare = Sample.CompareTo(other.Sample); - return compare is 0 ? Volume.CompareTo(other.Volume) : compare; - } + public int CompareTo(SoundEffectInfo other) => StartTime.CompareTo(other.StartTime); /// /// By-value comparer, auto-generated by Rider. diff --git a/Quaver.API/Maps/Structures/TimingPointInfo.cs b/Quaver.API/Maps/Structures/TimingPointInfo.cs index 8d3c8eb07..c33ac4277 100644 --- a/Quaver.API/Maps/Structures/TimingPointInfo.cs +++ b/Quaver.API/Maps/Structures/TimingPointInfo.cs @@ -71,27 +71,7 @@ public bool IsEditableInLuaScript public float MillisecondsPerBeat => 60000 / Bpm; /// - public int CompareTo(TimingPointInfo other) - { - if (ReferenceEquals(this, other)) - return 0; - - if (other is null) - return 1; - - var compare = StartTime.CompareTo(other.StartTime); - - if (compare != 0) - return compare; - - compare = Bpm.CompareTo(other.Bpm); - - if (compare != 0) - return compare; - - compare = Signature.CompareTo(other.Signature); - return compare is 0 ? Hidden.CompareTo(other.Hidden) : compare; - } + public int CompareTo(TimingPointInfo other) => StartTime.CompareTo(other.StartTime); /// /// By-value comparer, auto-generated by Rider. From 68bd5729ade3051aadbd1176ab50a368c7f1c647 Mon Sep 17 00:00:00 2001 From: Emik Date: Tue, 30 Jul 2024 19:27:50 +0200 Subject: [PATCH 16/25] Use stable sorting algorithms --- .../Quaver/Resources/stable-sorting.qua | 72 ++++++++++++++++ Quaver.API.Tests/Quaver/TestCaseQua.cs | 18 ++++ Quaver.API/Helpers/StartTimeHelper.cs | 86 ++++++++++++++++++- Quaver.API/Maps/Qua.cs | 21 ++--- Quaver.API/Maps/Structures/HitObjectInfo.cs | 27 +----- 5 files changed, 187 insertions(+), 37 deletions(-) create mode 100644 Quaver.API.Tests/Quaver/Resources/stable-sorting.qua 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..d633ac056 --- /dev/null +++ b/Quaver.API.Tests/Quaver/Resources/stable-sorting.qua @@ -0,0 +1,72 @@ +AudioFile: '' +SongPreviewTime: 0 +BackgroundFile: '' +MapId: -1 +MapSetId: -1 +Mode: Keys4 +Title: '' +Artist: '' +Source: '' +Tags: '' +DifficultyName: '' +EditorLayers: [] +Bookmarks: +- 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: 1 +- StartTime: 0 + Bpm: 2 +- StartTime: 0 + Bpm: 3 +- StartTime: 1000 + Bpm: 4 +- StartTime: 1000 + Bpm: 5 +- StartTime: 1000 + Bpm: 6 +SliderVelocities: +- StartTime: 0 + Multiplier: 1 +- StartTime: 0 + Multiplier: 2 +- StartTime: 0 + Multiplier: 3 +- StartTime: 1000 + Multiplier: 4 +- StartTime: 1000 + Multiplier: 5 +- StartTime: 1000 + Multiplier: 6 +HitObjects: +- StartTime: 0 + Lane: 1 + KeySounds: [] +- StartTime: 0 + Lane: 2 + KeySounds: [] +- StartTime: 0 + Lane: 3 + KeySounds: [] +- StartTime: 1000 + Lane: 4 + KeySounds: [] +- StartTime: 1000 + Lane: 5 + KeySounds: [] +- StartTime: 1000 + Lane: 6 + KeySounds: [] \ No newline at end of file diff --git a/Quaver.API.Tests/Quaver/TestCaseQua.cs b/Quaver.API.Tests/Quaver/TestCaseQua.cs index c58dd8165..7c1bea4cd 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() { diff --git a/Quaver.API/Helpers/StartTimeHelper.cs b/Quaver.API/Helpers/StartTimeHelper.cs index f6a472ae1..842967d4b 100644 --- a/Quaver.API/Helpers/StartTimeHelper.cs +++ b/Quaver.API/Helpers/StartTimeHelper.cs @@ -3,6 +3,7 @@ using System.Collections; using System.Collections.Generic; using System.Linq; +using System.Reflection; using System.Runtime.CompilerServices; using Quaver.API.Maps.Structures; @@ -10,6 +11,9 @@ 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 : IComparable { @@ -43,11 +47,45 @@ public static void InsertSorted(this List list, IEnumerable elements) break; default: // If the list ends up becoming large, it is no longer worth it to find the insertion. list.AddRange(elements); - list.Sort(); + 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 : IComparable + { + 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].CompareTo(list[j]) > 0) + { + (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. public static int IndexAtTime(this IList list, float time) @@ -126,6 +164,42 @@ private static void InsertSortedList(List list, IEnumerable elements, i } } + // ReSharper disable once CognitiveComplexity SuggestBaseTypeForParameter + private static void QuickSort(List list, int leftIndex, int rightIndex) + where T : IComparable + { + while (true) + { + var i = leftIndex; + var j = rightIndex; + var pivot = list[_rng.Next(leftIndex, rightIndex + 1)]; + + while (i <= j) + { + while (list[i].CompareTo(pivot) < 0) + i++; + + while (list[j].CompareTo(pivot) > 0) + 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)] private static int? TryCount(IEnumerable enumerable) => enumerable switch @@ -136,5 +210,15 @@ private static void InsertSortedList(List list, IEnumerable elements, i 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 3cb6b4f40..c4e855432 100644 --- a/Quaver.API/Maps/Qua.cs +++ b/Quaver.API/Maps/Qua.cs @@ -445,11 +445,11 @@ public List Validate() /// public void Sort() { - Bookmarks.Sort(); - HitObjects.Sort(); - SoundEffects.Sort(); - TimingPoints.Sort(); - SliderVelocities.Sort(); + SortBookmarks(); + SortHitObjects(); + SortSoundEffects(); + SortTimingPoints(); + SortSliderVelocities(); } /// @@ -957,23 +957,23 @@ public void MirrorHitObjects() /// /// - public void SortBookmarks() => Bookmarks.Sort(); + public void SortBookmarks() => Bookmarks.HybridSort(); /// /// - public void SortHitObjects() => HitObjects.Sort(); + public void SortHitObjects() => HitObjects.HybridSort(); /// /// - public void SortSliderVelocities() => SliderVelocities.Sort(); + public void SortSliderVelocities() => SliderVelocities.HybridSort(); /// /// - public void SortSoundEffects() => SoundEffects.Sort(); + public void SortSoundEffects() => SoundEffects.HybridSort(); /// /// - public void SortTimingPoints() => TimingPoints.Sort(); + public void SortTimingPoints() => TimingPoints.HybridSort(); /// /// Gets the judgement of a particular hitobject in the map @@ -1365,5 +1365,6 @@ private string GetFullPath(string file) => /// /// public string GetAudioPath() => GetFullPath(AudioFile); + } } diff --git a/Quaver.API/Maps/Structures/HitObjectInfo.cs b/Quaver.API/Maps/Structures/HitObjectInfo.cs index c5276ca65..ef85b8935 100644 --- a/Quaver.API/Maps/Structures/HitObjectInfo.cs +++ b/Quaver.API/Maps/Structures/HitObjectInfo.cs @@ -149,32 +149,7 @@ private void ThrowUneditableException() } /// - public int CompareTo(HitObjectInfo other) - { - if (ReferenceEquals(this, other)) - return 0; - - if (other is null) - return 1; - - var compare = StartTime.CompareTo(other.StartTime); - - if (compare != 0) - return compare; - - compare = Lane.CompareTo(other.Lane); - - if (compare != 0) - return compare; - - compare = EndTime.CompareTo(other.EndTime); - - if (compare != 0) - return compare; - - compare = HitSound.CompareTo(other.HitSound); - return compare is 0 ? EditorLayer.CompareTo(other.EditorLayer) : compare; - } + public int CompareTo(HitObjectInfo other) => EditorLayer.CompareTo(other.EditorLayer); /// /// By-value comparer, mostly auto-generated by Rider: KeySounds-related code is changed to by-value. From acf1fe9111eb5d832e4c0a410edf9933450cca93 Mon Sep 17 00:00:00 2001 From: Emik Date: Tue, 30 Jul 2024 19:29:07 +0200 Subject: [PATCH 17/25] Fix test --- .../Quaver/Resources/stable-sorting.qua | 169 ++++++++++++++++-- 1 file changed, 151 insertions(+), 18 deletions(-) diff --git a/Quaver.API.Tests/Quaver/Resources/stable-sorting.qua b/Quaver.API.Tests/Quaver/Resources/stable-sorting.qua index d633ac056..619274910 100644 --- a/Quaver.API.Tests/Quaver/Resources/stable-sorting.qua +++ b/Quaver.API.Tests/Quaver/Resources/stable-sorting.qua @@ -11,6 +11,28 @@ 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 @@ -27,46 +49,157 @@ CustomAudioSamples: [] SoundEffects: [] TimingPoints: - StartTime: 0 - Bpm: 1 + Bpm: 6 + Signature: 13646 - StartTime: 0 - Bpm: 2 + 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: 3 -- StartTime: 1000 Bpm: 4 + Signature: 3 +- StartTime: 0 + Bpm: 12 + Signature: 4 - StartTime: 1000 - Bpm: 5 + Bpm: 27 + Signature: 246 - StartTime: 1000 - Bpm: 6 + Bpm: 1 + Signature: 12 +- StartTime: 1000 + Bpm: 136 + Signature: 3785 SliderVelocities: - StartTime: 0 - Multiplier: 1 + Multiplier: 1253 +- StartTime: 0 + Multiplier: 1253 +- StartTime: 0 + Multiplier: 1253 - StartTime: 0 - Multiplier: 2 + Multiplier: 1253 - StartTime: 0 - Multiplier: 3 + 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: 4 + Multiplier: 675 - StartTime: 1000 - Multiplier: 5 + Multiplier: 0 - StartTime: 1000 - Multiplier: 6 + Multiplier: -13548 HitObjects: - StartTime: 0 - Lane: 1 + Lane: 4 KeySounds: [] + HitSound: Normal - StartTime: 0 - Lane: 2 + 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: 4 + Lane: 7 KeySounds: [] + HitSound: Normal - StartTime: 1000 - Lane: 5 + Lane: 1 KeySounds: [] + HitSound: Normal - StartTime: 1000 - Lane: 6 - KeySounds: [] \ No newline at end of file + Lane: 5 + KeySounds: [] + HitSound: Normal \ No newline at end of file From bbb73342f4e48eb17060e1396e850a440b159cf5 Mon Sep 17 00:00:00 2001 From: Emik Date: Thu, 1 Aug 2024 16:15:34 +0200 Subject: [PATCH 18/25] Apply lints --- Quaver.API/Helpers/StartTimeHelper.cs | 8 +- Quaver.API/Maps/Qua.cs | 345 ++++++++---------- Quaver.API/Maps/Structures/BookmarkInfo.cs | 13 +- .../Maps/Structures/CustomAudioSampleInfo.cs | 3 +- Quaver.API/Maps/Structures/EditorLayerInfo.cs | 10 +- Quaver.API/Maps/Structures/HitObjectInfo.cs | 284 +++++++------- Quaver.API/Maps/Structures/IStartTime.cs | 8 +- Quaver.API/Maps/Structures/KeySoundInfo.cs | 10 +- .../Maps/Structures/SliderVelocityInfo.cs | 1 + Quaver.API/Maps/Structures/SoundEffectInfo.cs | 1 + Quaver.API/Maps/Structures/TimingPointInfo.cs | 1 + 11 files changed, 325 insertions(+), 359 deletions(-) diff --git a/Quaver.API/Helpers/StartTimeHelper.cs b/Quaver.API/Helpers/StartTimeHelper.cs index 842967d4b..a8cd1d9f5 100644 --- a/Quaver.API/Helpers/StartTimeHelper.cs +++ b/Quaver.API/Helpers/StartTimeHelper.cs @@ -1,4 +1,10 @@ -// SPDX-License-Identifier: MPL-2.0 +/* + * 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; diff --git a/Quaver.API/Maps/Qua.cs b/Quaver.API/Maps/Qua.cs index c4e855432..742a73ae4 100644 --- a/Quaver.API/Maps/Qua.cs +++ b/Quaver.API/Maps/Qua.cs @@ -23,7 +23,7 @@ namespace Quaver.API.Maps { - [Serializable] + [Serializable] // ReSharper disable CognitiveComplexity CompareOfFloatsByEqualityOperator public class Qua { /// @@ -107,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). @@ -192,46 +192,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 @@ -284,6 +279,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(); @@ -293,74 +309,17 @@ 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(); - using var stringWriter = new StringWriter {NewLine = "\r\n"}; + var serializer = new Serializer(); // ReSharper disable once UsingStatementResourceInitialization + using var stringWriter = new StringWriter { NewLine = "\r\n" }; serializer.Serialize(stringWriter, this); var serialized = stringWriter.ToString(); @@ -557,10 +516,8 @@ public float GetCommonBpm() durations[point.Bpm] += duration; } - if (durations.Count is 0) // osu! hangs on loading the map in this case; we return a sensible result. - return TimingPoints[0].Bpm; - - 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; } /// @@ -625,21 +582,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(), + }; } /// @@ -659,16 +614,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]; @@ -676,7 +635,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. @@ -723,7 +682,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; @@ -734,6 +693,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; @@ -750,21 +710,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; @@ -778,6 +735,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); @@ -787,7 +745,7 @@ public void ApplyInverse() // For example, consider a fast section of the map transitioning into a very low BPM ending starting // with the next hit object. Since the LN release and the gap are still in the fast section, they // should use the fast section's BPM. - if ((int) Math.Round(timingPoint.StartTime) == nextObjectInLane.StartTime) + if ((int)Math.Round(timingPoint.StartTime) == nextObjectInLane.StartTime) { var prevTimingPointIndex = TimingPoints.IndexAtTimeBefore(timingPoint.StartTime); @@ -799,12 +757,10 @@ 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); + timeGap = (int?)Math.Max(Math.Round(15000 / bpm), MINIMAL_GAP_LENGTH); } // Summary of the changes: @@ -836,21 +792,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 @@ -858,27 +812,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); @@ -1021,8 +969,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; } @@ -1033,6 +983,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; @@ -1043,8 +994,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; } } @@ -1057,6 +1010,7 @@ public static void RestoreDefaultValues(Qua qua) private static void AfterLoad(Qua qua, bool checkValidity) { var errors = qua.Validate(); + if (checkValidity && errors.Count > 0) throw new ArgumentException(string.Join("\n", errors)); @@ -1090,9 +1044,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) { @@ -1100,6 +1053,7 @@ public void NormalizeSVs() break; var sv = SliderVelocities[currentSvIndex]; + if (sv.StartTime > timingPoint.StartTime) break; @@ -1121,11 +1075,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; } } @@ -1151,15 +1104,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++) @@ -1168,16 +1120,16 @@ 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; } normalizedScrollVelocities.Sort(); @@ -1214,12 +1166,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; @@ -1232,20 +1186,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; } } @@ -1263,14 +1216,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; @@ -1284,15 +1237,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++) @@ -1301,16 +1253,16 @@ 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; } InitialScrollVelocity = 0; @@ -1325,7 +1277,7 @@ public void DenormalizeSVs() /// public Qua WithNormalizedSVs() { - var qua = (Qua) MemberwiseClone(); + var qua = (Qua)MemberwiseClone(); // Relies on NormalizeSVs not changing anything within the by-reference members (but rather creating a new List). qua.NormalizeSVs(); return qua; @@ -1337,7 +1289,7 @@ public Qua WithNormalizedSVs() /// public Qua WithDenormalizedSVs() { - var qua = (Qua) MemberwiseClone(); + var qua = (Qua)MemberwiseClone(); // Relies on DenormalizeSVs not changing anything within the by-reference members (but rather creating a new List). qua.DenormalizeSVs(); return qua; @@ -1365,6 +1317,5 @@ private string GetFullPath(string file) => /// /// public string GetAudioPath() => GetFullPath(AudioFile); - } } diff --git a/Quaver.API/Maps/Structures/BookmarkInfo.cs b/Quaver.API/Maps/Structures/BookmarkInfo.cs index bc7974bf2..6ef1f04a8 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; @@ -38,13 +45,11 @@ 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(); 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 ef85b8935..e7afc105f 100644 --- a/Quaver.API/Maps/Structures/HitObjectInfo.cs +++ b/Quaver.API/Maps/Structures/HitObjectInfo.cs @@ -23,172 +23,150 @@ namespace Quaver.API.Maps.Structures [Serializable] public class HitObjectInfo : IComparable, IStartTime { - /// - /// The time in milliseconds when the HitObject is supposed to be hit. - /// - public int StartTime - { - get; - [MoonSharpVisible(false)] set; - } - - /// - /// The lane the HitObject falls in - /// - public int Lane - { - get; - [MoonSharpVisible(false)] set; - } - - /// - /// The endtime of the HitObject (if greater than 0, it's considered a hold note.) - /// - public int EndTime - { - get; - [MoonSharpVisible(false)] set; - } - - float IStartTime.StartTime - { - get => StartTime; - set => StartTime = (int)value; - } - - /// - /// Bitwise combination of hit sounds for this object - /// - public HitSounds HitSound - { - get; - [MoonSharpVisible(false)] set; - } - - /// - /// Key sounds to play when this object is hit. - /// - [MoonSharpVisible(false)] - public List KeySounds { get; set; } = new List(); - - /// - /// The layer in the editor that the object belongs to. - /// - public int EditorLayer - { - get; - [MoonSharpVisible(false)] set; - } - - /// - /// If the object is a long note. (EndTime > 0) - /// - [YamlIgnore] - public bool IsLongNote => EndTime > 0; - - /// - /// Returns if the object is allowed to be edited in lua scripts - /// - [YamlIgnore] - public bool IsEditableInLuaScript - { - get; - [MoonSharpVisible(false)] set; - } - - /// - /// Gets the timing point this object is in range of. - /// - /// - public TimingPointInfo GetTimingPoint(List timingPoints) => timingPoints.AtTime(StartTime); - - /// - /// - /// - /// - public void SetStartTime(int time) - { - ThrowUneditableException(); - StartTime = time; - } - - /// - /// - /// - public void SetEndTime(int time) - { - ThrowUneditableException(); - EndTime = time; - } + /// + /// The time in milliseconds when the HitObject is supposed to be hit. + /// + public int StartTime { get; [MoonSharpVisible(false)] set; } + + /// + /// The lane the HitObject falls in + /// + public int Lane { get; [MoonSharpVisible(false)] set; } + + /// + /// The endtime of the HitObject (if greater than 0, it's considered a hold note.) + /// + public int EndTime { get; [MoonSharpVisible(false)] set; } + + float IStartTime.StartTime + { + get => StartTime; + set => StartTime = (int)value; + } - /// - /// - /// - public void SetLane(int lane) - { - ThrowUneditableException(); - Lane = lane; - } + /// + /// Bitwise combination of hit sounds for this object + /// + public HitSounds HitSound { get; [MoonSharpVisible(false)] set; } + + /// + /// Key sounds to play when this object is hit. + /// + [MoonSharpVisible(false)] + public List KeySounds { get; set; } = new List(); + + /// + /// The layer in the editor that the object belongs to. + /// + public int EditorLayer { get; [MoonSharpVisible(false)] set; } + + /// + /// If the object is a long note. (EndTime > 0) + /// + [YamlIgnore] + public bool IsLongNote => EndTime > 0; + + /// + /// Returns if the object is allowed to be edited in lua scripts + /// + [YamlIgnore] + public bool IsEditableInLuaScript { get; [MoonSharpVisible(false)] set; } + + /// + /// Gets the timing point this object is in range of. + /// + /// + public TimingPointInfo GetTimingPoint(List timingPoints) => timingPoints.AtTime(StartTime); + + /// + /// + /// + /// + public void SetStartTime(int time) + { + ThrowUneditableException(); + StartTime = time; + } - /// - /// - /// - public void SetHitSounds(HitSounds hitsounds) - { - ThrowUneditableException(); - HitSound = hitsounds; - } + /// + /// + /// + public void SetEndTime(int time) + { + ThrowUneditableException(); + EndTime = time; + } - /// - /// - /// - private void ThrowUneditableException() - { - if (!IsEditableInLuaScript) - throw new InvalidOperationException("Value is not allowed to be edited in lua scripts."); - } + /// + /// + /// + public void SetLane(int lane) + { + ThrowUneditableException(); + Lane = lane; + } - /// - public int CompareTo(HitObjectInfo other) => EditorLayer.CompareTo(other.EditorLayer); + /// + /// + /// + public void SetHitSounds(HitSounds hitsounds) + { + ThrowUneditableException(); + HitSound = hitsounds; + } - /// - /// By-value comparer, mostly auto-generated by Rider: KeySounds-related code is changed to by-value. - /// - private sealed class ByValueEqualityComparer : IEqualityComparer - { - public bool Equals(HitObjectInfo x, HitObjectInfo y) + /// + /// + /// + private void ThrowUneditableException() { - if (ReferenceEquals(x, y)) return true; - 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; + if (!IsEditableInLuaScript) + throw new InvalidOperationException("Value is not allowed to be edited in lua scripts."); } - public int GetHashCode(HitObjectInfo obj) + /// + public int CompareTo(HitObjectInfo other) => EditorLayer.CompareTo(other.EditorLayer); + + /// + /// By-value comparer, mostly auto-generated by Rider: KeySounds-related code is changed to by-value. + /// + private sealed class ByValueEqualityComparer : IEqualityComparer { - unchecked + public bool Equals(HitObjectInfo x, HitObjectInfo y) { - var hashCode = obj.StartTime; - 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); + if (ReferenceEquals(x, y)) return true; + 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; + } - hashCode = (hashCode * 397) ^ obj.EditorLayer; - return hashCode; + public int GetHashCode(HitObjectInfo obj) + { + unchecked + { + var hashCode = obj.StartTime; + hashCode = (hashCode * 397) ^ obj.Lane; + hashCode = (hashCode * 397) ^ obj.EndTime; + hashCode = (hashCode * 397) ^ (int)obj.HitSound; + + hashCode = obj.KeySounds.Aggregate( + hashCode, + (current, keySound) => (current * 397) ^ KeySoundInfo.ByValueComparer.GetHashCode(keySound) + ); + + hashCode = (hashCode * 397) ^ obj.EditorLayer; + return hashCode; + } } } - } - public static IEqualityComparer ByValueComparer { get; } = new ByValueEqualityComparer(); + public static IEqualityComparer ByValueComparer { get; } = new ByValueEqualityComparer(); } } diff --git a/Quaver.API/Maps/Structures/IStartTime.cs b/Quaver.API/Maps/Structures/IStartTime.cs index e562eab73..50a827fdf 100644 --- a/Quaver.API/Maps/Structures/IStartTime.cs +++ b/Quaver.API/Maps/Structures/IStartTime.cs @@ -1,4 +1,10 @@ -// SPDX-License-Identifier: MPL-2.0 +/* + * 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 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 0caed7e61..603f3305b 100644 --- a/Quaver.API/Maps/Structures/SliderVelocityInfo.cs +++ b/Quaver.API/Maps/Structures/SliderVelocityInfo.cs @@ -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 1830b65be..feb75a647 100644 --- a/Quaver.API/Maps/Structures/SoundEffectInfo.cs +++ b/Quaver.API/Maps/Structures/SoundEffectInfo.cs @@ -45,6 +45,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; } diff --git a/Quaver.API/Maps/Structures/TimingPointInfo.cs b/Quaver.API/Maps/Structures/TimingPointInfo.cs index c33ac4277..cbd3548d2 100644 --- a/Quaver.API/Maps/Structures/TimingPointInfo.cs +++ b/Quaver.API/Maps/Structures/TimingPointInfo.cs @@ -84,6 +84,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; } From ff4341b66a10376360eb9db7646e985bc503ac8b Mon Sep 17 00:00:00 2001 From: Emik Date: Thu, 1 Aug 2024 16:19:28 +0200 Subject: [PATCH 19/25] Sort by StartTime --- Quaver.API/Maps/Structures/HitObjectInfo.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Quaver.API/Maps/Structures/HitObjectInfo.cs b/Quaver.API/Maps/Structures/HitObjectInfo.cs index e7afc105f..2210838da 100644 --- a/Quaver.API/Maps/Structures/HitObjectInfo.cs +++ b/Quaver.API/Maps/Structures/HitObjectInfo.cs @@ -125,7 +125,7 @@ private void ThrowUneditableException() } /// - public int CompareTo(HitObjectInfo other) => EditorLayer.CompareTo(other.EditorLayer); + public int CompareTo(HitObjectInfo other) => StartTime.CompareTo(other.StartTime); /// /// By-value comparer, mostly auto-generated by Rider: KeySounds-related code is changed to by-value. From f509e30f335562a9c551f5518d0c716d3c265344 Mon Sep 17 00:00:00 2001 From: Emik Date: Thu, 1 Aug 2024 17:09:03 +0200 Subject: [PATCH 20/25] Remove IComparable implementation, use IStartTime instead --- Quaver.API/Helpers/StartTimeHelper.cs | 33 +++++++++++-------- Quaver.API/Maps/Structures/BookmarkInfo.cs | 5 +-- Quaver.API/Maps/Structures/HitObjectInfo.cs | 5 +-- .../Maps/Structures/SliderVelocityInfo.cs | 5 +-- Quaver.API/Maps/Structures/SoundEffectInfo.cs | 5 +-- Quaver.API/Maps/Structures/TimingPointInfo.cs | 5 +-- 6 files changed, 25 insertions(+), 33 deletions(-) diff --git a/Quaver.API/Helpers/StartTimeHelper.cs b/Quaver.API/Helpers/StartTimeHelper.cs index a8cd1d9f5..d6defdac8 100644 --- a/Quaver.API/Helpers/StartTimeHelper.cs +++ b/Quaver.API/Helpers/StartTimeHelper.cs @@ -8,6 +8,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Diagnostics.Contracts; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; @@ -21,14 +22,14 @@ public static class StartTimeHelper static readonly Random _rng = Shared()?.GetMethod?.Invoke(null, null) as Random ?? new Random(); public static void InsertSorted(this List list, T element) - where T : IComparable + where T : IStartTime { var i = list.BinarySearch(element); list.Insert(i >= 0 ? i : ~i, element); } public static void InsertSorted(this List list, IEnumerable elements) - where T : IComparable + where T : IStartTime { // Thanks to @WilliamQiufeng for going through the trouble of benchmarking // to find the optimal capacity and count for our use case. @@ -69,7 +70,7 @@ public static void InsertSorted(this List list, IEnumerable elements) /// The type of list to sort. /// The list to sort. public static void HybridSort(this List list) - where T : IComparable + where T : IStartTime { var maxBacktracking = list.Count * (int)Math.Log(list.Count, 2); @@ -77,7 +78,7 @@ public static void HybridSort(this List list) { var j = i; - while (j > 0 && list[j - 1].CompareTo(list[j]) > 0) + while (j > 0 && list[j - 1].StartTime > list[j].StartTime) { (list[j], list[j - 1]) = (list[j - 1], list[j]); j--; @@ -94,7 +95,8 @@ public static void HybridSort(this List list) // Ideally would be IReadOnlyList to indicate no mutation, // but unfortunately IList doesn't implement IReadOnlyList. - public static int IndexAtTime(this IList list, float time) + [Pure] + public static int IndexAtTime(this List list, float time) where T : IStartTime { var left = 0; @@ -109,22 +111,26 @@ public static int IndexAtTime(this IList list, float time) return right; } - public static int IndexAtTimeBefore(this IList list, float time) + [Pure] + public static int IndexAtTimeBefore(this List list, float time) where T : IStartTime => IndexAtTime(list, Before(time)); - public static T AtTime(this IList list, float 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]; } - public static T AtTimeBefore(this IList list, float time) + [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. @@ -144,11 +150,12 @@ public static float After(float 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 : IComparable + where T : IStartTime { switch (elements) { @@ -172,7 +179,7 @@ private static void InsertSortedList(List list, IEnumerable elements, i // ReSharper disable once CognitiveComplexity SuggestBaseTypeForParameter private static void QuickSort(List list, int leftIndex, int rightIndex) - where T : IComparable + where T : IStartTime { while (true) { @@ -182,10 +189,10 @@ private static void QuickSort(List list, int leftIndex, int rightIndex) while (i <= j) { - while (list[i].CompareTo(pivot) < 0) + while (list[i].StartTime < pivot.StartTime) i++; - while (list[j].CompareTo(pivot) > 0) + while (list[j].StartTime > pivot.StartTime) j--; if (i > j) @@ -206,7 +213,7 @@ private static void QuickSort(List list, int leftIndex, int rightIndex) } } - [MethodImpl(MethodImplOptions.AggressiveInlining)] + [MethodImpl(MethodImplOptions.AggressiveInlining), Pure] private static int? TryCount(IEnumerable enumerable) => enumerable switch { diff --git a/Quaver.API/Maps/Structures/BookmarkInfo.cs b/Quaver.API/Maps/Structures/BookmarkInfo.cs index 6ef1f04a8..d6156b129 100644 --- a/Quaver.API/Maps/Structures/BookmarkInfo.cs +++ b/Quaver.API/Maps/Structures/BookmarkInfo.cs @@ -14,7 +14,7 @@ namespace Quaver.API.Maps.Structures { [MoonSharpUserData] [Serializable] - public class BookmarkInfo : IComparable, IStartTime + public class BookmarkInfo : IStartTime { public int StartTime { @@ -34,9 +34,6 @@ float IStartTime.StartTime set => StartTime = (int)value; } - /// - public int CompareTo(BookmarkInfo other) => StartTime.CompareTo(other.StartTime); - private sealed class TimeNoteEqualityComparer : IEqualityComparer { public bool Equals(BookmarkInfo x, BookmarkInfo y) diff --git a/Quaver.API/Maps/Structures/HitObjectInfo.cs b/Quaver.API/Maps/Structures/HitObjectInfo.cs index 2210838da..e72ba83c3 100644 --- a/Quaver.API/Maps/Structures/HitObjectInfo.cs +++ b/Quaver.API/Maps/Structures/HitObjectInfo.cs @@ -21,7 +21,7 @@ namespace Quaver.API.Maps.Structures /// [MoonSharpUserData] [Serializable] - public class HitObjectInfo : IComparable, IStartTime + public class HitObjectInfo : IStartTime { /// /// The time in milliseconds when the HitObject is supposed to be hit. @@ -124,9 +124,6 @@ private void ThrowUneditableException() throw new InvalidOperationException("Value is not allowed to be edited in lua scripts."); } - /// - public int CompareTo(HitObjectInfo other) => StartTime.CompareTo(other.StartTime); - /// /// By-value comparer, mostly auto-generated by Rider: KeySounds-related code is changed to by-value. /// diff --git a/Quaver.API/Maps/Structures/SliderVelocityInfo.cs b/Quaver.API/Maps/Structures/SliderVelocityInfo.cs index 603f3305b..591bbae5a 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 : IComparable, IStartTime + public class SliderVelocityInfo : IStartTime { /// /// The time in milliseconds when the new SliderVelocity section begins @@ -72,9 +72,6 @@ public void SetMultiplier(float multiplier) Multiplier = multiplier; } - /// - public int CompareTo(SliderVelocityInfo other) => StartTime.CompareTo(other.StartTime); - /// /// /// diff --git a/Quaver.API/Maps/Structures/SoundEffectInfo.cs b/Quaver.API/Maps/Structures/SoundEffectInfo.cs index feb75a647..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 : IComparable, IStartTime + public class SoundEffectInfo : IStartTime { /// /// The time at which to play the sound sample. @@ -31,9 +31,6 @@ public class SoundEffectInfo : IComparable, IStartTime /// public int Volume { get; set; } - /// - public int CompareTo(SoundEffectInfo other) => StartTime.CompareTo(other.StartTime); - /// /// By-value comparer, auto-generated by Rider. /// diff --git a/Quaver.API/Maps/Structures/TimingPointInfo.cs b/Quaver.API/Maps/Structures/TimingPointInfo.cs index cbd3548d2..0a820a7ac 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 : IComparable, IStartTime + public class TimingPointInfo : IStartTime { /// /// The time in milliseconds for when this timing point begins @@ -70,9 +70,6 @@ public bool IsEditableInLuaScript [YamlIgnore] public float MillisecondsPerBeat => 60000 / Bpm; - /// - public int CompareTo(TimingPointInfo other) => StartTime.CompareTo(other.StartTime); - /// /// By-value comparer, auto-generated by Rider. /// From 0a20d02c027bab1f54bd9c1d5196a833f435ea3c Mon Sep 17 00:00:00 2001 From: Emik Date: Thu, 1 Aug 2024 17:43:32 +0200 Subject: [PATCH 21/25] Use HybridSort --- Quaver.API/Maps/Qua.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Quaver.API/Maps/Qua.cs b/Quaver.API/Maps/Qua.cs index 742a73ae4..36b700ade 100644 --- a/Quaver.API/Maps/Qua.cs +++ b/Quaver.API/Maps/Qua.cs @@ -834,7 +834,7 @@ 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. - newHitObjects.Sort(); + newHitObjects.HybridSort(); HitObjects = newHitObjects; } @@ -1132,8 +1132,8 @@ public void NormalizeSVs() currentAdjustedSvMultiplier = multiplier; } - normalizedScrollVelocities.Sort(); BPMDoesNotAffectScrollVelocity = true; + normalizedScrollVelocities.HybridSort(); SliderVelocities = normalizedScrollVelocities; InitialScrollVelocity = initialSvMultiplier ?? 1; } @@ -1266,8 +1266,8 @@ public void DenormalizeSVs() } InitialScrollVelocity = 0; - denormalizedScrollVelocities.Sort(); BPMDoesNotAffectScrollVelocity = false; + denormalizedScrollVelocities.HybridSort(); SliderVelocities = denormalizedScrollVelocities; } From aeba7f18fc5ea58e198c6499635659316e9265d6 Mon Sep 17 00:00:00 2001 From: Emik Date: Thu, 1 Aug 2024 18:32:54 +0200 Subject: [PATCH 22/25] Do not use BinarySearch --- Quaver.API/Helpers/StartTimeHelper.cs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/Quaver.API/Helpers/StartTimeHelper.cs b/Quaver.API/Helpers/StartTimeHelper.cs index d6defdac8..8eb71ed8b 100644 --- a/Quaver.API/Helpers/StartTimeHelper.cs +++ b/Quaver.API/Helpers/StartTimeHelper.cs @@ -22,11 +22,8 @@ public static class StartTimeHelper 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 - { - var i = list.BinarySearch(element); - list.Insert(i >= 0 ? i : ~i, element); - } + where T : IStartTime => + list.Insert(IndexAtTime(list, element.StartTime) + 1, element); public static void InsertSorted(this List list, IEnumerable elements) where T : IStartTime From fc47da2688fd6f11ae4b92f63b99f1c7fa1d928c Mon Sep 17 00:00:00 2001 From: Emik Date: Thu, 1 Aug 2024 19:21:08 +0200 Subject: [PATCH 23/25] Fix concurrency issue --- Quaver.API/Helpers/ListHelper.cs | 50 ++++++++++++++++++++++++++++++++ Quaver.API/Maps/Qua.cs | 21 ++++++++++++-- 2 files changed, 69 insertions(+), 2 deletions(-) create mode 100644 Quaver.API/Helpers/ListHelper.cs 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/Maps/Qua.cs b/Quaver.API/Maps/Qua.cs index 36b700ade..563b44c5f 100644 --- a/Quaver.API/Maps/Qua.cs +++ b/Quaver.API/Maps/Qua.cs @@ -173,8 +173,7 @@ public class Qua /// /// [YamlIgnore] - public int Length => - HitObjects.Count is 0 ? 0 : Math.Max(HitObjects[^1].StartTime, HitObjects.Max(x => x.EndTime)); + public int Length => HitObjects.Count is 0 ? 0 : MaxObjectTime(); /// /// Integer based seed used for shuffling the lanes when randomize mod is active. @@ -1317,5 +1316,23 @@ private string GetFullPath(string file) => /// /// public string GetAudioPath() => GetFullPath(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; + } } } From cc89c8677e9a5c1871d250bbed684afb205fbbf2 Mon Sep 17 00:00:00 2001 From: Emik Date: Tue, 27 Aug 2024 15:32:43 +0200 Subject: [PATCH 24/25] Fix HitObjectInfo.GetTimingPoint returning null --- Quaver.API/Maps/Structures/HitObjectInfo.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Quaver.API/Maps/Structures/HitObjectInfo.cs b/Quaver.API/Maps/Structures/HitObjectInfo.cs index 42049a184..138aee157 100644 --- a/Quaver.API/Maps/Structures/HitObjectInfo.cs +++ b/Quaver.API/Maps/Structures/HitObjectInfo.cs @@ -101,7 +101,8 @@ public bool IsEditableInLuaScript /// Gets the timing point this object is in range of. /// /// - public TimingPointInfo GetTimingPoint(List timingPoints) => timingPoints.AtTime(StartTime); + public TimingPointInfo GetTimingPoint(List timingPoints) => + timingPoints.AtTime(StartTime) ?? timingPoints[0]; /// /// From c4a6ecb7f40399ef8daa62caa0bdc473a0668ebb Mon Sep 17 00:00:00 2001 From: Emik Date: Tue, 27 Aug 2024 15:33:46 +0200 Subject: [PATCH 25/25] Validate only when necessary --- Quaver.API/Maps/Qua.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Quaver.API/Maps/Qua.cs b/Quaver.API/Maps/Qua.cs index 563b44c5f..6954e66ff 100644 --- a/Quaver.API/Maps/Qua.cs +++ b/Quaver.API/Maps/Qua.cs @@ -1008,9 +1008,7 @@ public static void RestoreDefaultValues(Qua qua) /// private static void AfterLoad(Qua qua, bool checkValidity) { - var errors = qua.Validate(); - - if (checkValidity && errors.Count > 0) + if (checkValidity && qua.Validate() is var errors && errors.Count > 0) throw new ArgumentException(string.Join("\n", errors)); // Try to sort the Qua before returning.