Skip to content

Commit

Permalink
Merge pull request #186 from WilliamQiufeng/controller-group
Browse files Browse the repository at this point in the history
Add timing groups
  • Loading branch information
Swan authored Nov 2, 2024
2 parents d3840f9 + 2373031 commit 815b5f8
Show file tree
Hide file tree
Showing 8 changed files with 256 additions and 16 deletions.
4 changes: 4 additions & 0 deletions Quaver.API/Maps/AutoMod/AutoMod.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
using Quaver.API.Maps.AutoMod.Issues.Map;
using Quaver.API.Maps.AutoMod.Issues.Metadata;
using Quaver.API.Maps.AutoMod.Issues.ScrollVelocities;
using Quaver.API.Maps.AutoMod.Issues.TimingGroups;
using Quaver.API.Maps.AutoMod.Issues.TimingPoints;
using Quaver.API.Maps.Structures;
using Quaver.API.Replays;
Expand Down Expand Up @@ -134,6 +135,9 @@ private void DetectHitObjectIssues()
var hitObject = Qua.HitObjects[i];
var laneIndex = hitObject.Lane - 1;

if (!Qua.TimingGroups.ContainsKey(hitObject.TimingGroup))
Issues.Add(new AutoModIssueObjectInvalidTimingGroup(hitObject));

// Check if the long note is too short
if (hitObject.IsLongNote && Math.Abs(hitObject.EndTime - hitObject.StartTime) < ShortLongNoteThreshold)
Issues.Add(new AutoModIssueShortLongNote(hitObject));
Expand Down
3 changes: 2 additions & 1 deletion Quaver.API/Maps/AutoMod/Issues/AutoModIssueCategory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ public enum AutoModIssueCategory
Mapset,
Metadata,
ScrollVelocities,
TimingPoints
TimingPoints,
TimingGroups
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using Quaver.API.Maps.Structures;

namespace Quaver.API.Maps.AutoMod.Issues.HitObjects
{
public class AutoModIssueObjectInvalidTimingGroup : AutoModIssue
{
/// <summary>
/// </summary>
public HitObjectInfo HitObjectInfo { get; }

public AutoModIssueObjectInvalidTimingGroup(HitObjectInfo hitObjectInfo) : base(AutoModIssueLevel.Critical)
{
HitObjectInfo = hitObjectInfo;
Text = $"The note in column {hitObjectInfo.Lane} at {hitObjectInfo.StartTime} has " +
$"invalid timing group id '{hitObjectInfo.TimingGroup}'.";
}

public override AutoModIssueCategory Category { get; protected set; } = AutoModIssueCategory.HitObjects;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using Quaver.API.Maps.Structures;

namespace Quaver.API.Maps.AutoMod.Issues.TimingGroups
{
public class AutoModIssueDuplicateTimingGroupId : AutoModIssue
{
/// <summary>
/// </summary>
public string Id { get; }

public AutoModIssueDuplicateTimingGroupId(string id) : base(
AutoModIssueLevel.Critical)
{
Id = id;
Text = $"Duplicate timing group ID '{id}'.";
}

public override AutoModIssueCategory Category { get; protected set; } = AutoModIssueCategory.TimingGroups;
}
}
144 changes: 129 additions & 15 deletions Quaver.API/Maps/Qua.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* 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 <[email protected]>.
*/
*/

using System;
using System.Collections.Generic;
Expand Down Expand Up @@ -123,7 +123,11 @@ public class Qua
///
/// Only matters if BPMDoesNotAffectScrollVelocity is true.
/// </summary>
public float InitialScrollVelocity { get; set; }
public float InitialScrollVelocity
{
get => DefaultScrollGroup.InitialScrollVelocity;
set => DefaultScrollGroup.InitialScrollVelocity = value;
}

/// <summary>
/// If true, the map will have a +1 scratch key, allowing for 5/8 key play
Expand Down Expand Up @@ -161,13 +165,38 @@ public class Qua
/// Note that SVs can be both in normalized and denormalized form, depending on BPMDoesNotAffectSV.
/// Check WithNormalizedSVs if you need normalized SVs.
/// </summary>
public List<SliderVelocityInfo> SliderVelocities { get; private set; } = new List<SliderVelocityInfo>();
public List<SliderVelocityInfo> SliderVelocities
{
get => DefaultScrollGroup.ScrollVelocities;
private set => DefaultScrollGroup.ScrollVelocities = value;
}

/// <summary>
/// HitObject .qua data
/// </summary>
public List<HitObjectInfo> HitObjects { get; private set; } = new List<HitObjectInfo>();

public Dictionary<string, TimingGroup> TimingGroups { get; private set; } =
new Dictionary<string, TimingGroup>();

[YamlIgnore]
public ScrollGroup DefaultScrollGroup { get; } = new ScrollGroup
{
ColorRgb = "86,254,110"
};

[YamlIgnore] public ScrollGroup GlobalScrollGroup { get; } = new ScrollGroup();

/// <summary>
/// Reserved ID for default scroll group
/// </summary>
public const string DefaultScrollGroupId = "";

/// <summary>
/// Reserved ID for global scroll group (applied to every scroll groups)
/// </summary>
public const string GlobalScrollGroupId = "*";

/// <summary>
/// Finds the length of the map
/// </summary>
Expand All @@ -191,7 +220,20 @@ public class Qua
/// <summary>
/// Ctor
/// </summary>
public Qua() { }
public Qua()
{
LinkDefaultScrollGroup();
}

/// <summary>
/// Link <see cref="DefaultScrollGroup"/> to <see cref="TimingGroups"/>
/// so <see cref="TimingGroups"/>[<see cref="DefaultScrollGroupId"/>] points to that group.
/// </summary>
private void LinkDefaultScrollGroup()
{
TimingGroups[DefaultScrollGroupId] = DefaultScrollGroup;
TimingGroups[GlobalScrollGroupId] = GlobalScrollGroup;
}

/// <summary>
/// Returns true if the two maps are equal by value.
Expand All @@ -217,6 +259,7 @@ public bool EqualByValue(Qua other) =>
TimingPoints.SequenceEqual(other.TimingPoints, TimingPointInfo.ByValueComparer) &&
SliderVelocities.SequenceEqual(other.SliderVelocities, SliderVelocityInfo.ByValueComparer) &&
InitialScrollVelocity == other.InitialScrollVelocity &&
TimingGroups.SequenceEqual(other.TimingGroups) &&
BPMDoesNotAffectScrollVelocity == other.BPMDoesNotAffectScrollVelocity &&
LegacyLNRendering == other.LegacyLNRendering &&
HasScratchKey == other.HasScratchKey &&
Expand All @@ -227,6 +270,17 @@ public bool EqualByValue(Qua other) =>
Bookmarks.SequenceEqual(other.Bookmarks, BookmarkInfo.ByValueComparer) &&
RandomizeModifierSeed == other.RandomizeModifierSeed;

private static IDeserializer Deserializer =>
new DeserializerBuilder()
.IgnoreUnmatchedProperties()
.WithTagMapping("!ScrollGroup", typeof(ScrollGroup))
.Build();

private static ISerializer Serializer =>
new SerializerBuilder()
.WithTagMapping("!ScrollGroup", typeof(ScrollGroup))
.Build();

/// <summary>
/// Loads a .qua file from a stream
/// </summary>
Expand All @@ -237,9 +291,7 @@ public static Qua Parse(byte[] buffer, bool checkValidity = true)
{
using var input = new StringReader(Encoding.UTF8.GetString(buffer, 0, buffer.Length));

var deserializer = new DeserializerBuilder();
deserializer.IgnoreUnmatchedProperties();
var qua = (Qua)deserializer.Build().Deserialize(input, typeof(Qua));
var qua = (Qua)Deserializer.Deserialize(input, typeof(Qua));

RestoreDefaultValues(qua);
AfterLoad(qua, checkValidity);
Expand All @@ -259,9 +311,7 @@ public static Qua Parse(string path, bool checkValidity = true)

using (var file = File.OpenText(path))
{
var deserializer = new DeserializerBuilder();
deserializer.IgnoreUnmatchedProperties();
qua = (Qua)deserializer.Build().Deserialize(file, typeof(Qua));
qua = (Qua)Deserializer.Deserialize(file, typeof(Qua));
qua.FilePath = path;

RestoreDefaultValues(qua);
Expand All @@ -287,6 +337,7 @@ static HitObjectInfo SerializableHitObject(HitObjectInfo obj) =>
.Select(x => new KeySoundInfo { Sample = x.Sample, Volume = x.Volume == 100 ? 0 : x.Volume })
.ToList(),
Lane = obj.Lane, StartTime = obj.StartTime,
TimingGroup = obj.TimingGroup == DefaultScrollGroupId ? null : obj.TimingGroup
};

static SoundEffectInfo SerializableSoundEffect(SoundEffectInfo x) =>
Expand All @@ -307,19 +358,30 @@ x.Signature is TimeSignature.Quadruple
var originalHitObjects = HitObjects;
var originalSoundEffects = SoundEffects;
var originalBookmarks = Bookmarks;
var originalTimingGroups = TimingGroups;

TimingPoints = originalTimingPoints.Select(SerializableTimingPoint).ToList();
HitObjects = originalHitObjects.Select(SerializableHitObject).ToList();
SoundEffects = originalSoundEffects.Select(SerializableSoundEffect).ToList();
TimingGroups = originalTimingGroups.ToDictionary(x => x.Key, x => x.Value);
TimingGroups.Remove(DefaultScrollGroupId);

// Remove empty global scroll group
var globalScrollGroup = GlobalScrollGroup;
if (globalScrollGroup.ScrollVelocities.Count == 0)
TimingGroups.Remove(GlobalScrollGroupId);

// Don't serialize the field at all if we dont have additional timing groups
if (TimingGroups.Count == 0)
TimingGroups = null;

// 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(); // ReSharper disable once UsingStatementResourceInitialization
using var stringWriter = new StringWriter { NewLine = "\r\n" };
serializer.Serialize(stringWriter, this);
Serializer.Serialize(stringWriter, this);
var serialized = stringWriter.ToString();

// Restore the original lists.
Expand All @@ -328,6 +390,10 @@ x.Signature is TimeSignature.Quadruple
SoundEffects = originalSoundEffects;
Bookmarks = originalBookmarks;

TimingGroups = originalTimingGroups;
LinkDefaultScrollGroup();
Debug.Assert(TimingGroups != null && TimingGroups.Count >= 2);

return serialized;
}

Expand Down Expand Up @@ -403,6 +469,8 @@ public List<string> Validate()
/// </summary>
public void Sort()
{
LinkDefaultScrollGroup();

SortBookmarks();
SortHitObjects();
SortSoundEffects();
Expand Down Expand Up @@ -544,8 +612,10 @@ public TimingPointInfo GetTimingPointAt(double time)
/// Gets a scroll velocity at a particular time in the map
/// </summary>
/// <param name="time"></param>
/// <param name="timingGroupId"></param>
/// <returns></returns>
public SliderVelocityInfo GetScrollVelocityAt(double time) => SliderVelocities.AtTime((float)time);
public SliderVelocityInfo GetScrollVelocityAt(double time, string timingGroupId = DefaultScrollGroupId) =>
((ScrollGroup)TimingGroups[timingGroupId]).GetScrollVelocityAt(time);

/// <summary>
/// Finds the length of a timing point.
Expand All @@ -570,6 +640,36 @@ public double GetTimingPointLength(TimingPointInfo point)
return Length - point.StartTime;
}

/// <summary>
/// O(n)
/// Returns the list of hit objects that are in the specified group
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
public IEnumerable<HitObjectInfo> GetTimingGroupObjects(string id) =>
HitObjects.Where(hitObjectInfo => hitObjectInfo.TimingGroup == id);

/// <summary>
/// O(n log m) Given a list of timing group IDs, return a dictionary of (ID, [HitObject])
/// </summary>
/// <param name="timingGroupIds"></param>
/// <returns></returns>
public Dictionary<string, List<HitObjectInfo>> GetTimingGroupObjects(HashSet<string> timingGroupIds)
{
var result = new Dictionary<string, List<HitObjectInfo>>();
foreach (var hitObjectInfo in HitObjects.Where(hitObjectInfo =>
timingGroupIds.Contains(hitObjectInfo.TimingGroup)))
{
if (!result.TryGetValue(hitObjectInfo.TimingGroup, out var list))
{
list = new List<HitObjectInfo>();
result.Add(hitObjectInfo.TimingGroup, list);
}
list.Add(hitObjectInfo);
}
return result;
}

/// <summary>
/// Solves the difficulty of the map and returns the data for it.
/// </summary>
Expand Down Expand Up @@ -912,7 +1012,15 @@ public void MirrorHitObjects()

/// <summary>
/// </summary>
public void SortSliderVelocities() => SliderVelocities.HybridSort();
public void SortSliderVelocities()
{
foreach (var (_, timingGroup) in TimingGroups)
{
if (!(timingGroup is ScrollGroup scrollGroup))
continue;
scrollGroup.ScrollVelocities.HybridSort();
}
}

/// <summary>
/// </summary>
Expand Down Expand Up @@ -964,6 +1072,10 @@ public HitObjectInfo GetHitObjectAtJudgementIndex(int index)
/// <param name="qua"></param>
public static void RestoreDefaultValues(Qua qua)
{
qua.LinkDefaultScrollGroup();

qua.TimingGroups.TryAdd(GlobalScrollGroupId, new ScrollGroup());

// Restore default values.
for (var i = 0; i < qua.TimingPoints.Count; i++)
{
Expand All @@ -982,6 +1094,8 @@ public static void RestoreDefaultValues(Qua qua)
if (obj.HitSound == 0)
obj.HitSound = HitSounds.Normal;

obj.TimingGroup ??= DefaultScrollGroupId;

// ReSharper disable once ForeachCanBePartlyConvertedToQueryUsingAnotherGetEnumerator
foreach (var keySound in obj.KeySounds)
if (keySound.Volume == 0)
Expand Down Expand Up @@ -1338,4 +1452,4 @@ private int MaxObjectTime()
return max;
}
}
}
}
9 changes: 9 additions & 0 deletions Quaver.API/Maps/Structures/HitObjectInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,15 @@ public int EditorLayer
set;
}

/// <summary>
/// ID of the timing group this note belongs to. Leave empty for default timing group.
/// </summary>
public string TimingGroup
{
get;
[MoonSharpVisible(false)] set;
} = Qua.DefaultScrollGroupId;

/// <summary>
/// If the object is a long note. (EndTime > 0)
/// </summary>
Expand Down
Loading

0 comments on commit 815b5f8

Please sign in to comment.