diff --git a/MainWindow.xaml.cs b/MainWindow.xaml.cs
index 7158024..b6d58a8 100644
--- a/MainWindow.xaml.cs
+++ b/MainWindow.xaml.cs
@@ -252,6 +252,6 @@ namespace RDTKEditor
}
public class MainViewModel : INotifyPropertyChanged
{
- public event PropertyChangedEventHandler PropertyChanged;
+ public event PropertyChangedEventHandler? PropertyChanged;
}
}
\ No newline at end of file
diff --git a/RDTKEditor.csproj b/RDTKEditor.csproj
index 7d41591..875c821 100644
--- a/RDTKEditor.csproj
+++ b/RDTKEditor.csproj
@@ -9,8 +9,13 @@
-
+
+
+
+
+
+
diff --git a/RhythmBaseCore/Adofai/Components/ADBeat.cs b/RhythmBaseCore/Adofai/Components/ADBeat.cs
new file mode 100644
index 0000000..1e7dffc
--- /dev/null
+++ b/RhythmBaseCore/Adofai/Components/ADBeat.cs
@@ -0,0 +1,269 @@
+using RhythmBase.Adofai.Utils;
+using RhythmBase.Exceptions;
+using System.Diagnostics.CodeAnalysis;
+namespace RhythmBase.Adofai.Components
+{
+ ///
+ /// Represents a beat in the ADLevel.
+ ///
+ public struct ADBeat : IComparable, IEquatable
+ {
+ internal readonly ADLevel? baseLevel => _calculator?.Collection;
+
+ ///
+ /// Gets or sets the beat only value.
+ ///
+ public readonly float BeatOnly
+ {
+ get => _beat + 1f;
+
+ set
+ {
+ }
+ }
+
+ ///
+ /// Gets or sets the time span.
+ ///
+ public readonly TimeSpan TimeSpan
+ {
+ get => _timeSpan;
+
+ set
+ {
+ }
+ }
+
+ ///
+ /// Initializes a new instance of the struct with a specified beat.
+ ///
+ /// The beat value.
+ public ADBeat(float beat)
+ {
+ this = default;
+ _beat = beat;
+ _isBeatLoaded = true;
+ }
+
+ ///
+ /// Initializes a new instance of the struct with a specified time span.
+ ///
+ /// The time span value.
+ public ADBeat(TimeSpan timeSpan)
+ {
+ this = default;
+ _timeSpan = timeSpan;
+ _isTimeSpanLoaded = true;
+ }
+
+ ///
+ /// Initializes a new instance of the struct with a specified calculator and beat.
+ ///
+ /// The beat calculator.
+ /// The beat value.
+ public ADBeat(ADBeatCalculator calculator, float beat)
+ {
+ this = default;
+ _calculator = calculator;
+ _beat = beat;
+ _isBeatLoaded = true;
+ }
+
+ ///
+ /// Initializes a new instance of the struct with a specified calculator and time span.
+ ///
+ /// The beat calculator.
+ /// The time span value.
+ /// Thrown when the time span is less than zero.
+ public ADBeat(ADBeatCalculator calculator, TimeSpan timeSpan)
+ {
+ this = default;
+ if (timeSpan < TimeSpan.Zero)
+ {
+ throw new OverflowException(string.Format("The time must not be less than zero, but {0} is given", timeSpan));
+ }
+ _calculator = calculator;
+ _timeSpan = timeSpan;
+ _isTimeSpanLoaded = true;
+ }
+ ///
+ /// Construct a beat of the 1st beat from the calculator
+ ///
+ /// Specified calculator.
+ /// The first beat tied to the level.
+ public static ADBeat Default(ADBeatCalculator calculator)
+ {
+ ADBeat Default = new(calculator, 1f);
+ return Default;
+ }
+ ///
+ /// Determine if two beats come from the same level
+ ///
+ /// A beat.
+ /// Another beat.
+ /// If true, an exception will be thrown when two beats do not come from the same level.
+ ///
+ public static bool FromSameLevel(ADBeat a, ADBeat b, bool @throw = false)
+ {
+ bool flag = a.baseLevel.Equals(b.baseLevel);
+ bool FromSameLevel;
+ if (flag)
+ {
+ FromSameLevel = true;
+ }
+ else
+ {
+ if (@throw)
+ {
+ throw new RhythmBaseException("Beats must come from the same ADLevel.");
+ }
+ FromSameLevel = false;
+ }
+ return FromSameLevel;
+ }
+ ///
+ /// Determine if two beats are from the same level.
+ ///
+ /// If any of them does not come from any level, it will also return true.
+ ///
+ /// A beat.
+ /// Another beat.
+ /// If true, an exception will be thrown when two beats do not come from the same level.
+ ///
+ public static bool FromSameLevelOrNull(ADBeat a, ADBeat b, bool @throw = false) => a.baseLevel == null || b.baseLevel == null || FromSameLevel(a, b, @throw);
+
+ public readonly bool FromSameLevel(ADBeat b, bool @throw = false) => FromSameLevel(this, b, @throw);
+ ///
+ /// Determine if two beats are from the same level.
+ ///
+ /// If any of them does not come from any level, it will also return true.
+ ///
+ /// Another beat.
+ /// If true, an exception will be thrown when two beats do not come from the same level.
+ ///
+ public readonly bool FromSameLevelOrNull(ADBeat b, bool @throw = false) => baseLevel == null || b.baseLevel == null || FromSameLevel(b, @throw);
+ ///
+ /// Returns a new instance of unbinding the level.
+ ///
+ /// A new instance of unbinding the level.
+ public readonly ADBeat WithoutBinding()
+ {
+ ADBeat result = this;
+ result._calculator = null;
+ return result;
+ }
+
+ private readonly void IfNullThrowException()
+ {
+ if (IsEmpty)
+ {
+ throw new InvalidRDBeatException();
+ }
+ }
+ ///
+ /// Refresh the cache.
+ ///
+ public void ResetCache()
+ {
+ float i = BeatOnly;
+ _isTimeSpanLoaded = false;
+ }
+
+ internal void ResetBPM()
+ {
+ _isBeatLoaded = true;
+ _isTimeSpanLoaded = false;
+ _isBpmLoaded = false;
+ }
+
+ internal void ResetCPB() => _isBeatLoaded = true;
+
+ ///
+ /// Gets a value indicating whether this instance is empty.
+ ///
+ ///
+ /// true if this instance is empty; otherwise, false.
+ ///
+ public readonly bool IsEmpty
+ {
+ get
+ {
+ return _calculator == null || (!_isBeatLoaded && !_isTimeSpanLoaded);
+ }
+ }
+
+ ///
+ public static ADBeat operator +(ADBeat a, float b)
+ {
+ ADBeat result = new(a._calculator, a.BeatOnly + b);
+ return result;
+ }
+
+ ///
+ public static ADBeat operator +(ADBeat a, TimeSpan b)
+ {
+ ADBeat result = new(a._calculator, a.TimeSpan + b);
+ return result;
+ }
+
+ ///
+ public static ADBeat operator -(ADBeat a, float b)
+ {
+ ADBeat result = new(a._calculator, a.BeatOnly - b);
+ return result;
+ }
+
+ ///
+ public static ADBeat operator -(ADBeat a, TimeSpan b)
+ {
+ ADBeat result = new(a._calculator, a.TimeSpan - b);
+ return result;
+ }
+ ///
+ public static bool operator >(ADBeat a, ADBeat b) => FromSameLevel(a, b, true) && a.BeatOnly > b.BeatOnly;
+ ///
+
+ public static bool operator <(ADBeat a, ADBeat b) => FromSameLevel(a, b, true) && a.BeatOnly < b.BeatOnly;
+ ///
+
+ public static bool operator >=(ADBeat a, ADBeat b) => FromSameLevel(a, b, true) && a.BeatOnly >= b.BeatOnly;
+ ///
+
+ public static bool operator <=(ADBeat a, ADBeat b) => FromSameLevel(a, b, true) && a.BeatOnly <= b.BeatOnly;
+
+ ///
+ public static bool operator ==(ADBeat a, ADBeat b) => (FromSameLevel(a, b, true) && a._beat == b._beat) || (a._isTimeSpanLoaded && b._isTimeSpanLoaded && a._timeSpan == b._timeSpan) || a.BeatOnly == b.BeatOnly;
+
+ ///
+ public static bool operator !=(ADBeat a, ADBeat b) => !(a == b);
+
+ ///
+ public readonly int CompareTo(ADBeat other) => checked((int)Math.Round((double)unchecked(_beat - other._beat)));
+
+ ///
+ public override readonly string ToString() => string.Format("[{0}]", BeatOnly);
+
+ ///
+ public override readonly bool Equals([NotNull] object obj) => obj.GetType() == typeof(ADBeat) && Equals((obj != null) ? ((ADBeat)obj) : default);
+
+ ///
+ public readonly bool Equals(ADBeat other) => this == other;
+
+ ///
+ public override readonly int GetHashCode() => HashCode.Combine(BeatOnly, baseLevel);
+
+ internal ADBeatCalculator? _calculator;
+
+ private bool _isBeatLoaded;
+
+ private bool _isTimeSpanLoaded;
+
+ private bool _isBpmLoaded;
+
+ private float _beat;
+
+ private TimeSpan _timeSpan;
+
+ private float _bpm;
+ }
+}
diff --git a/RhythmBaseCore/Adofai/Components/ADLevel.cs b/RhythmBaseCore/Adofai/Components/ADLevel.cs
new file mode 100644
index 0000000..1d5d78a
--- /dev/null
+++ b/RhythmBaseCore/Adofai/Components/ADLevel.cs
@@ -0,0 +1,97 @@
+using Microsoft.VisualBasic.CompilerServices;
+using Newtonsoft.Json;
+using RhythmBase.Adofai.Converters;
+using RhythmBase.Adofai.Events;
+using RhythmBase.Adofai.Utils;
+using RhythmBase.Exceptions;
+using RhythmBase.Settings;
+namespace RhythmBase.Adofai.Components
+{
+ ///
+ /// Adofal level.
+ ///
+ public class ADLevel : ADTileCollection
+ {
+ ///
+ /// Level settings.
+ ///
+ public ADSettings Settings { get; set; }
+ ///
+ /// Level decoration collection.
+ ///
+ public List Decorations { get; set; }
+ ///
+ /// Level file path.
+ ///
+ [JsonIgnore]
+ public string Path => _path;
+ ///
+ /// Level directory path.
+ ///
+ [JsonIgnore]
+ public string Directory => System.IO.Path.GetDirectoryName(_path);
+ ///
+ /// Get all the events of the level.
+ ///
+ public override IEnumerable Events
+ {
+ get
+ {
+ foreach (ADBaseEvent tile in base.Events)
+ yield return tile;
+ foreach (ADBaseEvent tile2 in Decorations)
+ yield return tile2;
+ }
+ }
+ ///
+ /// The calculator that comes with the level.
+ ///
+ [JsonIgnore]
+ public ADBeatCalculator Calculator { get; }
+ public ADLevel()
+ {
+ Settings = new ADSettings();
+ Decorations = [];
+ Calculator = new ADBeatCalculator(this);
+ }
+ public ADLevel(IEnumerable items)
+ {
+ Settings = new ADSettings();
+ Decorations = [];
+ Calculator = new ADBeatCalculator(this);
+ foreach (ADTile tile in items)
+ Add(tile);
+ }
+ ///
+ /// The default level within the game.
+ ///
+ public static ADLevel Default => [];
+ ///
+ /// Read from file as level.
+ /// Use default input settings.
+ /// Supports .rdlevel, .rdzip, .zip file extension.
+ ///
+ /// File path.
+ /// An instance of a level that reads from a file.
+ public static ADLevel Read(string filepath) => Read(filepath, new LevelReadOrWriteSettings());
+ ///
+ /// Read from file as level.
+ /// Supports .rdlevel, .rdzip, .zip file extension.
+ ///
+ /// File path.
+ /// Input settings.
+ /// An instance of a level that reads from a file.
+ public static ADLevel Read(string filepath, LevelReadOrWriteSettings settings)
+ {
+ JsonSerializer LevelSerializer = new();
+ LevelSerializer.Converters.Add(new ADLevelConverter(filepath, settings));
+ string extension = System.IO.Path.GetExtension(filepath);
+ if (extension != ".adofai")
+ {
+ throw new RhythmBaseException("File not supported.");
+ }
+ return LevelSerializer.Deserialize(new JsonTextReader(File.OpenText(filepath)))!;
+ }
+ internal string _path;
+ }
+}
diff --git a/RhythmBaseCore/Adofai/Components/ADSettings.cs b/RhythmBaseCore/Adofai/Components/ADSettings.cs
new file mode 100644
index 0000000..057ff0b
--- /dev/null
+++ b/RhythmBaseCore/Adofai/Components/ADSettings.cs
@@ -0,0 +1,145 @@
+using System;
+using System.Collections.Generic;
+using RhythmBase.Adofai.Events;
+using RhythmBase.Components;
+using RhythmBase.Components.Easing;
+namespace RhythmBase.Adofai.Components
+{
+ public class ADSettings
+ {
+ public ADSettings()
+ {
+ RequiredMods = [];
+ }
+
+ public int Version { get; set; }
+
+ public string Artist { get; set; }
+
+ public SpecialArtistTypes SpecialArtistType { get; set; }
+
+ public string ArtistPermission { get; set; }
+
+ public string Song { get; set; }
+
+ public string Author { get; set; }
+
+ public bool SeparateCountdownTime { get; set; }
+
+ public string PreviewImage { get; set; }
+
+ public string PreviewIcon { get; set; }
+
+ public RDColor PreviewIconColor { get; set; }
+
+ public float PreviewSongStart { get; set; }
+
+ public float PreviewSongDuration { get; set; }
+
+ public bool SeizureWarning { get; set; }
+
+ public string LevelDesc { get; set; }
+
+ public string LevelTags { get; set; }
+
+ public string ArtistLinks { get; set; }
+
+ public int Difficulty { get; set; }
+
+ public List RequiredMods { get; set; }
+
+ public string SongFilename { get; set; }
+
+ public float Bpm { get; set; }
+
+ public float Volume { get; set; }
+
+ public float Offset { get; set; }
+
+ public float Pitch { get; set; }
+
+ public string Hitsound { get; set; }
+
+ public float HitsoundVolume { get; set; }
+
+ public float CountdownTicks { get; set; }
+
+ public ADTrackColorTypes TrackColorType { get; set; }
+
+ public RDColor TrackColor { get; set; }
+
+ public RDColor SecondaryTrackColor { get; set; }
+
+ public float TrackColorAnimDuration { get; set; }
+
+ public ADTrackColorPulses TrackColorPulse { get; set; }
+
+ public float TrackPulseLength { get; set; }
+
+ public ADTrackStyles TrackStyle { get; set; }
+
+ public ADTrackAnimationTypes TrackAnimation { get; set; }
+
+ public int BeatsAhead { get; set; }
+
+ public ADTrackDisappearAnimationTypes TrackDisappearAnimation { get; set; }
+
+ public int BeatsBehind { get; set; }
+
+ public RDColor BackgroundColor { get; set; }
+
+ public bool ShowDefaultBGIfNoImage { get; set; }
+
+ public string BgImage { get; set; }
+
+ public RDColor BgImageColor { get; set; }
+
+ public RDPointI Parallax { get; set; }
+
+ public BgDisplayModes BgDisplayMode { get; set; }
+
+ public bool LockRot { get; set; }
+
+ public bool LoopBG { get; set; }
+
+ public float ScalingRatio { get; set; }
+
+ public ADCameraRelativeTo RelativeTo { get; set; }
+
+ public RDPointI Position { get; set; }
+
+ public float Rotation { get; set; }
+
+ public float Zoom { get; set; }
+
+ public string BgVideo { get; set; }
+
+ public bool LoopVideo { get; set; }
+
+ public int VidOffset { get; set; }
+
+ public bool FloorIconOutlines { get; set; }
+
+ public bool StickToFloors { get; set; }
+
+ public EaseType PlanetEase { get; set; }
+
+ public int PlanetEaseParts { get; set; }
+
+ public ADEasePartBehaviors PlanetEasePartBehavior { get; set; }
+
+ public RDColor DefaultTextColor { get; set; }
+
+ public RDColor DefaultTextShadowColor { get; set; }
+
+ public string CongratsText { get; set; }
+
+ public string PerfectText { get; set; }
+
+ public bool LegacyFlash { get; set; }
+
+ public bool LegacyCamRelativeTo { get; set; }
+
+ public bool LegacySpriteTiles { get; set; }
+ }
+}
diff --git a/RhythmBaseCore/Adofai/Components/ADTileCollection.cs b/RhythmBaseCore/Adofai/Components/ADTileCollection.cs
new file mode 100644
index 0000000..5aefbec
--- /dev/null
+++ b/RhythmBaseCore/Adofai/Components/ADTileCollection.cs
@@ -0,0 +1,43 @@
+using RhythmBase.Adofai.Events;
+using System.Collections;
+namespace RhythmBase.Adofai.Components
+{
+ public abstract class ADTileCollection : ICollection
+ {
+ protected ADTileCollection()
+ {
+ tileOrder = [];
+ IsReadOnly = false;
+ EndTile = [];
+ }
+ public int Count => tileOrder.Count;
+ public bool IsReadOnly { get; }
+ public ADTile EndTile { get; }
+ public ADTile this[int index] => index == tileOrder.Count ? EndTile : tileOrder[index];
+ public virtual IEnumerable Events
+ {
+ get
+ {
+ foreach (ADTile tile in tileOrder)
+ foreach (ADBaseTileEvent action in tile)
+ yield return action;
+ foreach (ADBaseTileEvent action2 in EndTile)
+ yield return action2;
+ }
+ }
+ public void Add(ADTile item) => tileOrder.Add(item);
+ public void Clear() => tileOrder.Clear();
+ public void CopyTo(ADTile[] array, int arrayIndex) => tileOrder.CopyTo(array, arrayIndex);
+ public bool Contains(ADTile item) => tileOrder.Contains(item);
+ public bool Remove(ADTile item) => tileOrder.Remove(item);
+ public IEnumerator GetEnumerator() => tileOrder.GetEnumerator();
+ ///
+ /// Get the index of tile.
+ ///
+ /// The index of tile.
+ ///
+ public int IndexOf(ADTile item) => item == EndTile ? Count : tileOrder.IndexOf(item);
+ IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
+ internal List tileOrder;
+ }
+}
diff --git a/RhythmBaseCore/Adofai/Components/ADTrackAnimationTypes.cs b/RhythmBaseCore/Adofai/Components/ADTrackAnimationTypes.cs
new file mode 100644
index 0000000..6f20529
--- /dev/null
+++ b/RhythmBaseCore/Adofai/Components/ADTrackAnimationTypes.cs
@@ -0,0 +1,16 @@
+using System;
+namespace RhythmBase.Adofai.Components
+{
+ public enum ADTrackAnimationTypes
+ {
+ None,
+ Assemble,
+ Assemble_Far,
+ Extend,
+ Grow,
+ Grow_Spin,
+ Fade,
+ Drop,
+ Rise
+ }
+}
diff --git a/RhythmBaseCore/Adofai/Components/ADTrackDisappearAnimationTypes.cs b/RhythmBaseCore/Adofai/Components/ADTrackDisappearAnimationTypes.cs
new file mode 100644
index 0000000..b49db41
--- /dev/null
+++ b/RhythmBaseCore/Adofai/Components/ADTrackDisappearAnimationTypes.cs
@@ -0,0 +1,14 @@
+using System;
+namespace RhythmBase.Adofai.Components
+{
+ public enum ADTrackDisappearAnimationTypes
+ {
+ None,
+ Scatter,
+ Scatter_Far,
+ Retract,
+ Shrink,
+ Shrink_Spin,
+ Fade
+ }
+}
diff --git a/RhythmBaseCore/Adofai/Components/ADTypedList.cs b/RhythmBaseCore/Adofai/Components/ADTypedList.cs
new file mode 100644
index 0000000..5e497c6
--- /dev/null
+++ b/RhythmBaseCore/Adofai/Components/ADTypedList.cs
@@ -0,0 +1,28 @@
+using RhythmBase.Adofai.Events;
+using System.Collections;
+namespace RhythmBase.Adofai.Components
+{
+ public class ADTypedList : IEnumerable where TEvent : ADBaseEvent
+ {
+ public ADTypedList()
+ {
+ list = [];
+ _types = [];
+ }
+ public void Add(TEvent item)
+ {
+ list.Add(item);
+ _types.Add(item.Type);
+ }
+ public object Remove(TEvent item)
+ {
+ _types.Remove(item.Type);
+ return list.Remove(item);
+ }
+ public override string ToString() => string.Format("Count = {0}", list.Count);
+ public IEnumerator GetEnumerator() => list.GetEnumerator();
+ IEnumerator IEnumerable.GetEnumerator() => list.GetEnumerator();
+ private readonly List list;
+ protected internal HashSet _types;
+ }
+}
diff --git a/RhythmBaseCore/Adofai/Components/BgDisplayModes.cs b/RhythmBaseCore/Adofai/Components/BgDisplayModes.cs
new file mode 100644
index 0000000..408ddaa
--- /dev/null
+++ b/RhythmBaseCore/Adofai/Components/BgDisplayModes.cs
@@ -0,0 +1,10 @@
+using System;
+namespace RhythmBase.Adofai.Components
+{
+ public enum BgDisplayModes
+ {
+ FitToScreen,
+ Unscaled,
+ Tiled
+ }
+}
diff --git a/RhythmBaseCore/Adofai/Converters/ADBaseEventConverter.cs b/RhythmBaseCore/Adofai/Converters/ADBaseEventConverter.cs
new file mode 100644
index 0000000..38b093f
--- /dev/null
+++ b/RhythmBaseCore/Adofai/Converters/ADBaseEventConverter.cs
@@ -0,0 +1,60 @@
+using System;
+using Microsoft.VisualBasic.CompilerServices;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+using RhythmBase.Adofai.Components;
+using RhythmBase.Adofai.Events;
+using RhythmBase.Adofai.Utils;
+using RhythmBase.Settings;
+namespace RhythmBase.Adofai.Converters
+{
+ internal class ADBaseEventConverter(ADLevel level, LevelReadOrWriteSettings inputSettings) : JsonConverter where TEvent : ADBaseEvent
+ {
+ public override bool CanRead
+ {
+ get
+ {
+ return _canread;
+ }
+ }
+
+ public override bool CanWrite
+ {
+ get
+ {
+ return _canwrite;
+ }
+ }
+ public override void WriteJson(JsonWriter writer, TEvent? value, JsonSerializer serializer) => throw new NotImplementedException();
+
+ public override TEvent ReadJson(JsonReader reader, Type objectType, TEvent? existingValue, bool hasExistingValue, JsonSerializer serializer) => GetDeserializedObject((JObject)JToken.ReadFrom(reader), objectType, existingValue, hasExistingValue, serializer);
+
+ public virtual TEvent GetDeserializedObject(JObject jobj, Type objectType, TEvent existingValue, bool hasExistingValue, JsonSerializer serializer)
+ {
+ Type SubClassType = Utils.Utils.ADConvertToType(jobj["eventType"].ToObject());
+ _canread = false;
+ existingValue = Conversions.ToGenericParameter((SubClassType != null) ? jobj.ToObject(SubClassType, serializer) : jobj.ToObject(serializer));
+ _canread = true;
+ return existingValue;
+ }
+
+ public virtual JObject SetSerializedObject(TEvent value, JsonSerializer serializer)
+ {
+ _canwrite = false;
+ JObject JObj = JObject.FromObject(value, serializer);
+ _canwrite = true;
+ JObj.Remove("type");
+ JToken s = JObj.First;
+ s.AddBeforeSelf(new JProperty("eventType", value.Type.ToString()));
+ return JObj;
+ }
+
+ protected readonly ADLevel level = level;
+
+ protected readonly LevelReadOrWriteSettings settings = inputSettings;
+
+ protected bool _canread = true;
+
+ protected bool _canwrite = true;
+ }
+}
diff --git a/RhythmBaseCore/Adofai/Converters/ADBaseTileEventConverter.cs b/RhythmBaseCore/Adofai/Converters/ADBaseTileEventConverter.cs
new file mode 100644
index 0000000..859efb2
--- /dev/null
+++ b/RhythmBaseCore/Adofai/Converters/ADBaseTileEventConverter.cs
@@ -0,0 +1,42 @@
+using System;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+using RhythmBase.Adofai.Components;
+using RhythmBase.Adofai.Events;
+using RhythmBase.Adofai.Utils;
+using RhythmBase.Settings;
+namespace RhythmBase.Adofai.Converters
+{
+ internal class ADBaseTileEventConverter(ADLevel level, LevelReadOrWriteSettings inputSettings) : ADBaseEventConverter(level, inputSettings) where TEvent : ADBaseTileEvent
+ {
+ public override TEvent GetDeserializedObject(JObject jobj, Type objectType, TEvent existingValue, bool hasExistingValue, JsonSerializer serializer)
+ {
+ JToken jtoken = jobj["floor"];
+ int? parentIndex = (jtoken != null) ? new int?(jtoken.ToObject()) : null;
+ _canread = false;
+ if (Utils.Utils.ADConvertToType(jobj["eventType"].ToObject()) == typeof(ADCustomEvent))
+ {
+ existingValue = (TEvent)(object)new ADCustomTileEventConverter(level, settings).GetDeserializedObject(jobj, objectType, null, hasExistingValue, serializer);
+ }
+ else
+ {
+ jobj.Remove("floor");
+ existingValue = base.GetDeserializedObject(jobj, objectType, existingValue, hasExistingValue, serializer);
+ }
+ _canread = true;
+ if (parentIndex != null)
+ {
+ existingValue.Parent = level[parentIndex.Value];
+ existingValue.Parent.Add((ADBaseTileEvent)(object)existingValue);
+ }
+ return existingValue;
+ }
+ public override JObject SetSerializedObject(TEvent value, JsonSerializer serializer)
+ {
+ JObject jobj = base.SetSerializedObject(value, serializer);
+ JToken s = jobj.First;
+ s.AddBeforeSelf(new JProperty("floor", level.tileOrder.IndexOf(value.Parent)));
+ return jobj;
+ }
+ }
+}
diff --git a/RhythmBaseCore/Adofai/Converters/ADCustomEventConverter.cs b/RhythmBaseCore/Adofai/Converters/ADCustomEventConverter.cs
new file mode 100644
index 0000000..d998d91
--- /dev/null
+++ b/RhythmBaseCore/Adofai/Converters/ADCustomEventConverter.cs
@@ -0,0 +1,16 @@
+using System;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+using RhythmBase.Adofai.Components;
+using RhythmBase.Adofai.Events;
+using RhythmBase.Settings;
+namespace RhythmBase.Adofai.Converters
+{
+ internal class ADCustomEventConverter(ADLevel level, LevelReadOrWriteSettings settings) : ADBaseEventConverter(level, settings)
+ {
+ public override ADCustomEvent GetDeserializedObject(JObject jobj, Type objectType, ADCustomEvent existingValue, bool hasExistingValue, JsonSerializer serializer) => new()
+ {
+ Data = jobj
+ };
+ }
+}
diff --git a/RhythmBaseCore/Adofai/Converters/ADCustomTileEventConverter.cs b/RhythmBaseCore/Adofai/Converters/ADCustomTileEventConverter.cs
new file mode 100644
index 0000000..0601803
--- /dev/null
+++ b/RhythmBaseCore/Adofai/Converters/ADCustomTileEventConverter.cs
@@ -0,0 +1,17 @@
+using System;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+using RhythmBase.Adofai.Components;
+using RhythmBase.Adofai.Events;
+using RhythmBase.Settings;
+namespace RhythmBase.Adofai.Converters
+{
+ internal class ADCustomTileEventConverter(ADLevel level, LevelReadOrWriteSettings settings) : ADBaseTileEventConverter(level, settings)
+ {
+ public override ADCustomTileEvent GetDeserializedObject(JObject jobj, Type objectType, ADCustomTileEvent existingValue, bool hasExistingValue, JsonSerializer serializer) => new()
+ {
+ Parent = level[jobj["floor"].ToObject()],
+ Data = jobj
+ };
+ }
+}
diff --git a/RhythmBaseCore/Adofai/Converters/ADLevelConverter.cs b/RhythmBaseCore/Adofai/Converters/ADLevelConverter.cs
new file mode 100644
index 0000000..6c5b689
--- /dev/null
+++ b/RhythmBaseCore/Adofai/Converters/ADLevelConverter.cs
@@ -0,0 +1,87 @@
+using Newtonsoft.Json;
+using Newtonsoft.Json.Converters;
+using Newtonsoft.Json.Linq;
+using Newtonsoft.Json.Serialization;
+using RhythmBase.Adofai.Components;
+using RhythmBase.Adofai.Events;
+using RhythmBase.Adofai.Utils;
+using RhythmBase.Extensions;
+using RhythmBase.Converters;
+using RhythmBase.Settings;
+namespace RhythmBase.Adofai.Converters
+{
+ internal class ADLevelConverter(string location, LevelReadOrWriteSettings settings) : JsonConverter
+ {
+ public override void WriteJson(JsonWriter writer, ADLevel? value, JsonSerializer serializer)
+ {
+ JsonSerializerSettings AllInOneSerializer = new()
+ {
+ ContractResolver = new CamelCasePropertyNamesContractResolver(),
+ Formatting = Formatting.None
+ };
+ IList converters = AllInOneSerializer.Converters;
+ converters.Add(new StringEnumConverter());
+ converters.Add(new ColorConverter());
+ writer.Formatting = settings.Indented ? Formatting.Indented : Formatting.None;
+ writer.WriteStartObject();
+ writer.WritePropertyName("angleData");
+ writer.WriteStartArray();
+ foreach (ADTile item in value!)
+ writer.WriteRawValue(JsonConvert.SerializeObject(item.Angle, Formatting.None));
+ writer.WriteEndArray();
+ writer.WritePropertyName("settings");
+ writer.WriteRawValue(JsonConvert.SerializeObject(value.Settings, Formatting.Indented, AllInOneSerializer));
+ writer.WritePropertyName("actions");
+ writer.WriteStartArray();
+ foreach (ADTile item2 in value)
+ writer.WriteRawValue(JsonConvert.SerializeObject(item2, Formatting.None, AllInOneSerializer));
+ writer.WriteEndArray();
+ writer.WritePropertyName("decorations");
+ writer.WriteStartArray();
+ foreach (ADBaseEvent item3 in value.Decorations)
+ writer.WriteRawValue(JsonConvert.SerializeObject(item3, Formatting.None, AllInOneSerializer));
+ writer.WriteEndArray();
+ writer.WriteEndObject();
+ writer.Close();
+ }
+ public override ADLevel ReadJson(JsonReader reader, Type objectType, ADLevel? existingValue, bool hasExistingValue, JsonSerializer serializer)
+ {
+ ADLevel outLevel = new()
+ {
+ _path = fileLocation
+ };
+ JsonSerializer AllInOneSerializer = outLevel.GetSerializer(settings);
+ JArray JActions = [];
+ JArray JDecorations = [];
+ while (reader.Read())
+ {
+ string name = (string)reader.Value!;
+ reader.Read();
+ object left = name;
+ switch (name)
+ {
+ case "settings":
+ JObject jobj = JObject.Load(reader);
+ outLevel.Settings = jobj.ToObject(AllInOneSerializer)!;
+ break;
+ case "angleData":
+ JArray jobj2 = JArray.Load(reader);
+ //outLevel.AddRange(jobj2.ToObject>(AllInOneSerializer)!);
+ break;
+ case "actions":
+ JActions = JArray.Load(reader);
+ break;
+ case "decorations":
+ JDecorations = JArray.Load(reader);
+ break;
+ }
+ }
+ reader.Close();
+ JActions.ToObject>(AllInOneSerializer);
+ outLevel.Decorations.AddRange(JDecorations.ToObject>(AllInOneSerializer)!);
+ return outLevel;
+ }
+ private readonly string fileLocation = location;
+ private readonly LevelReadOrWriteSettings settings = settings;
+ }
+}
diff --git a/RhythmBaseCore/Adofai/Converters/ADTileConverter.cs b/RhythmBaseCore/Adofai/Converters/ADTileConverter.cs
new file mode 100644
index 0000000..ffadb5d
--- /dev/null
+++ b/RhythmBaseCore/Adofai/Converters/ADTileConverter.cs
@@ -0,0 +1,20 @@
+using System;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+using RhythmBase.Adofai.Components;
+using RhythmBase.Adofai.Events;
+namespace RhythmBase.Adofai.Converters
+{
+ internal class ADTileConverter(ADLevel level) : JsonConverter
+ {
+ public override void WriteJson(JsonWriter writer, ADTile value, JsonSerializer serializer) => throw new NotImplementedException();
+
+ public override ADTile ReadJson(JsonReader reader, Type objectType, ADTile existingValue, bool hasExistingValue, JsonSerializer serializer) => new()
+ {
+ Angle = JToken.Load(reader).ToObject(),
+ Parent = level
+ };
+
+ private readonly ADLevel level = level;
+ }
+}
diff --git a/RhythmBaseCore/Adofai/Events/ADAddDecoration.cs b/RhythmBaseCore/Adofai/Events/ADAddDecoration.cs
new file mode 100644
index 0000000..edb4422
--- /dev/null
+++ b/RhythmBaseCore/Adofai/Events/ADAddDecoration.cs
@@ -0,0 +1,122 @@
+using System;
+using RhythmBase.Components;
+namespace RhythmBase.Adofai.Events
+{
+ public class ADAddDecoration : ADBaseTileEvent
+ {
+ public ADAddDecoration()
+ {
+ Type = ADEventType.AddDecoration;
+ }
+
+ public override ADEventType Type { get; }
+
+ public string DecorationImage { get; set; }
+
+ public RDPointN Position { get; set; }
+
+ public ADDecorationRelativeTo RelativeTo { get; set; }
+
+ public RDSizeN PivotOffset { get; set; }
+
+ public float Rotation { get; set; }
+
+ public bool LockRotation { get; set; }
+
+ public RDSizeN Scale { get; set; }
+
+ public bool LockScale { get; set; }
+
+ public RDSizeN Tile { get; set; }
+
+ public RDColor Color { get; set; }
+
+ public float Opacity { get; set; }
+
+ public int Depth { get; set; }
+
+ public RDSizeN Parallax { get; set; }
+
+ public RDSizeN ParallaxOffset { get; set; }
+
+ public string Tag { get; set; }
+
+ public bool ImageSmoothing { get; set; }
+
+ public BlendModes BlendMode { get; set; }
+
+ public MaskingTypes MaskingType { get; set; }
+
+ public bool UseMaskingDepth { get; set; }
+
+ public int MaskingFrontDepth { get; set; }
+
+ public int MaskingBackDepth { get; set; }
+
+ public HitboxTypes Hitbox { get; set; }
+
+ public string HitboxEventTag { get; set; }
+
+ public FailHitboxTypes FailHitboxType { get; set; }
+
+ public RDSizeN FailHitboxScale { get; set; }
+
+ public RDSizeN FailHitboxOffset { get; set; }
+
+ public int FailHitboxRotation { get; set; }
+
+ public string Components { get; set; }
+
+ public enum BlendModes
+ {
+ None,
+ Darken,
+ Multiply,
+ ColorBurn,
+ LinearBurn,
+ DarkerColor,
+ Lighten,
+ Screen,
+ ColorDodge,
+ LinearDodge,
+ LighterColor,
+ Overlay,
+ SoftLight,
+ HardLight,
+ VividLight,
+ LinearLight,
+ PinLight,
+ HardMix,
+ Difference,
+ Exclusion,
+ Subtract,
+ Divide,
+ Hue,
+ Saturation,
+ Color,
+ Luminosity
+ }
+
+ public enum MaskingTypes
+ {
+ None,
+ Mask,
+ VisibleInsideMask,
+ VisibleOutsideMask
+ }
+
+ public enum HitboxTypes
+ {
+ None,
+ Kill,
+ Event
+ }
+
+ public enum FailHitboxTypes
+ {
+ Box,
+ Circle,
+ Capsule
+ }
+ }
+}
diff --git a/RhythmBaseCore/Adofai/Events/ADAddObject.cs b/RhythmBaseCore/Adofai/Events/ADAddObject.cs
new file mode 100644
index 0000000..9b36410
--- /dev/null
+++ b/RhythmBaseCore/Adofai/Events/ADAddObject.cs
@@ -0,0 +1,95 @@
+using System;
+using RhythmBase.Components;
+namespace RhythmBase.Adofai.Events
+{
+ public class ADAddObject : ADBaseEvent
+ {
+ public ADAddObject()
+ {
+ Type = ADEventType.AddObject;
+ }
+
+ public override ADEventType Type { get; }
+
+ public ObjectTypes ObjectType { get; set; }
+
+ public PlanetColorTypes PlanetColorType { get; set; }
+
+ public RDColor PlanetColor { get; set; }
+
+ public RDColor PlanetTailColor { get; set; }
+
+ public TrackTypes TrackType { get; set; }
+
+ public float TrackAngle { get; set; }
+
+ public ADTrackColorTypes TrackColorType { get; set; }
+
+ public RDColor TrackColor { get; set; }
+
+ public RDColor SecondaryTrackColor { get; set; }
+
+ public float TrackColorAnimDuration { get; set; }
+
+ public float TrackOpacity { get; set; }
+
+ public ADTrackStyles TrackStyle { get; set; }
+
+ public string TrackIcon { get; set; }
+
+ public float TrackIconAngle { get; set; }
+
+ public bool TrackRedSwirl { get; set; }
+
+ public bool TrackGraySetSpeedIcon { get; set; }
+
+ public float TrackSetSpeedIconBpm { get; set; }
+
+ public bool TrackGlowEnabled { get; set; }
+
+ public RDColor TrackGlowColor { get; set; }
+
+ public RDPointN Position { get; set; }
+
+ public ADCameraRelativeTo RelativeTo { get; set; }
+
+ public RDSizeN PivotOffset { get; set; }
+
+ public float Rotation { get; set; }
+
+ public bool LockRotation { get; set; }
+
+ public RDSizeN Scale { get; set; }
+
+ public bool LockScale { get; set; }
+
+ public int Depth { get; set; }
+
+ public RDSizeN Parallax { get; set; }
+
+ public RDSizeN ParallaxOffset { get; set; }
+
+ public string Tag { get; set; }
+
+ public enum ObjectTypes
+ {
+ Floor,
+ Planet
+ }
+
+ public enum PlanetColorTypes
+ {
+ DefaultRed,
+ planetColorType,
+ Gold,
+ Overseer,
+ Custom
+ }
+
+ public enum TrackTypes
+ {
+ Normal,
+ Midspin
+ }
+ }
+}
diff --git a/RhythmBaseCore/Adofai/Events/ADAddText.cs b/RhythmBaseCore/Adofai/Events/ADAddText.cs
new file mode 100644
index 0000000..c82c2e4
--- /dev/null
+++ b/RhythmBaseCore/Adofai/Events/ADAddText.cs
@@ -0,0 +1,44 @@
+using System;
+using RhythmBase.Components;
+namespace RhythmBase.Adofai.Events
+{
+ public class ADAddText : ADBaseEvent
+ {
+ public ADAddText()
+ {
+ Type = ADEventType.AddText;
+ }
+
+ public override ADEventType Type { get; }
+
+ public string DecText { get; set; }
+
+ public string Font { get; set; }
+
+ public RDPointN Position { get; set; }
+
+ public ADCameraRelativeTo RelativeTo { get; set; }
+
+ public RDSizeN PivotOffset { get; set; }
+
+ public float Rotation { get; set; }
+
+ public bool LockRotation { get; set; }
+
+ public RDSizeN Scale { get; set; }
+
+ public bool LockScale { get; set; }
+
+ public RDColor Color { get; set; }
+
+ public float Opacity { get; set; }
+
+ public int Depth { get; set; }
+
+ public RDSizeN Parallax { get; set; }
+
+ public RDSizeN ParallaxOffset { get; set; }
+
+ public string Tag { get; set; }
+ }
+}
diff --git a/RhythmBaseCore/Adofai/Events/ADAnimateTrack.cs b/RhythmBaseCore/Adofai/Events/ADAnimateTrack.cs
new file mode 100644
index 0000000..0be0b7f
--- /dev/null
+++ b/RhythmBaseCore/Adofai/Events/ADAnimateTrack.cs
@@ -0,0 +1,47 @@
+using System;
+using Newtonsoft.Json;
+namespace RhythmBase.Adofai.Events
+{
+ [JsonObject(ItemNullValueHandling = NullValueHandling.Ignore)]
+ public class ADAnimateTrack : ADBaseTileEvent
+ {
+ public ADAnimateTrack()
+ {
+ Type = ADEventType.AnimateTrack;
+ }
+
+ public override ADEventType Type { get; }
+
+ public TrackAnimations? TrackAnimation { get; set; }
+
+ public TrackDisappearAnimations? TrackDisappearAnimation { get; set; }
+
+ public int BeatsAhead { get; set; }
+
+ public int BeatsBehind { get; set; }
+
+ public enum TrackAnimations
+ {
+ None,
+ Assemble,
+ Assemble_Far,
+ Extend,
+ Grow,
+ Grow_Spin,
+ Fade,
+ Drop,
+ Rise
+ }
+
+ public enum TrackDisappearAnimations
+ {
+ None,
+ Scatter,
+ Scatter_Far,
+ Retract,
+ Shrink,
+ Shrink_Spin,
+ Fade
+ }
+ }
+}
diff --git a/RhythmBaseCore/Adofai/Events/ADAutoPlayTiles.cs b/RhythmBaseCore/Adofai/Events/ADAutoPlayTiles.cs
new file mode 100644
index 0000000..d28c99e
--- /dev/null
+++ b/RhythmBaseCore/Adofai/Events/ADAutoPlayTiles.cs
@@ -0,0 +1,19 @@
+using System;
+namespace RhythmBase.Adofai.Events
+{
+ public class ADAutoPlayTiles : ADBaseTileEvent
+ {
+ public ADAutoPlayTiles()
+ {
+ Type = ADEventType.AutoPlayTiles;
+ }
+
+ public override ADEventType Type { get; }
+
+ public bool Enabled { get; set; }
+
+ public bool ShowStatusText { get; set; }
+
+ public bool SafetyTiles { get; set; }
+ }
+}
diff --git a/RhythmBaseCore/Adofai/Events/ADBaseEvent.cs b/RhythmBaseCore/Adofai/Events/ADBaseEvent.cs
new file mode 100644
index 0000000..34e01d6
--- /dev/null
+++ b/RhythmBaseCore/Adofai/Events/ADBaseEvent.cs
@@ -0,0 +1,10 @@
+using System;
+namespace RhythmBase.Adofai.Events
+{
+ public abstract class ADBaseEvent
+ {
+ public abstract ADEventType Type { get; }
+
+ public override string ToString() => Type.ToString();
+ }
+}
diff --git a/RhythmBaseCore/Adofai/Events/ADBaseTaggedTileAction.cs b/RhythmBaseCore/Adofai/Events/ADBaseTaggedTileAction.cs
new file mode 100644
index 0000000..4dcddc1
--- /dev/null
+++ b/RhythmBaseCore/Adofai/Events/ADBaseTaggedTileAction.cs
@@ -0,0 +1,10 @@
+using System;
+namespace RhythmBase.Adofai.Events
+{
+ public abstract class ADBaseTaggedTileAction : ADBaseTileEvent
+ {
+ public float AngleOffset { get; set; }
+
+ public string EventTag { get; set; }
+ }
+}
diff --git a/RhythmBaseCore/Adofai/Events/ADBaseTileEvent.cs b/RhythmBaseCore/Adofai/Events/ADBaseTileEvent.cs
new file mode 100644
index 0000000..73e02cb
--- /dev/null
+++ b/RhythmBaseCore/Adofai/Events/ADBaseTileEvent.cs
@@ -0,0 +1,12 @@
+using System;
+using Newtonsoft.Json;
+namespace RhythmBase.Adofai.Events
+{
+ public abstract class ADBaseTileEvent : ADBaseEvent
+ {
+ [JsonIgnore]
+ public ADTile Parent { get; set; }
+
+ public override string ToString() => string.Format("{0}", Type);
+ }
+}
diff --git a/RhythmBaseCore/Adofai/Events/ADBloom.cs b/RhythmBaseCore/Adofai/Events/ADBloom.cs
new file mode 100644
index 0000000..e9dbe26
--- /dev/null
+++ b/RhythmBaseCore/Adofai/Events/ADBloom.cs
@@ -0,0 +1,28 @@
+using System;
+using RhythmBase.Components;
+using RhythmBase.Components.Easing;
+using RhythmBase.Events;
+namespace RhythmBase.Adofai.Events
+{
+ public class ADBloom : ADBaseTaggedTileAction, IEaseEvent
+ {
+ public ADBloom()
+ {
+ Type = ADEventType.Bloom;
+ }
+
+ public override ADEventType Type { get; }
+
+ public bool Enabled { get; set; }
+
+ public int Threshold { get; set; }
+
+ public int Intensity { get; set; }
+
+ public RDColor Color { get; set; }
+
+ public float Duration { get; set; }
+
+ public EaseType Ease { get; set; }
+ }
+}
diff --git a/RhythmBaseCore/Adofai/Events/ADBookmark.cs b/RhythmBaseCore/Adofai/Events/ADBookmark.cs
new file mode 100644
index 0000000..75638d3
--- /dev/null
+++ b/RhythmBaseCore/Adofai/Events/ADBookmark.cs
@@ -0,0 +1,13 @@
+using System;
+namespace RhythmBase.Adofai.Events
+{
+ public class ADBookmark : ADBaseTileEvent
+ {
+ public ADBookmark()
+ {
+ Type = ADEventType.Bookmark;
+ }
+
+ public override ADEventType Type { get; }
+ }
+}
diff --git a/RhythmBaseCore/Adofai/Events/ADCameraRelativeTo.cs b/RhythmBaseCore/Adofai/Events/ADCameraRelativeTo.cs
new file mode 100644
index 0000000..9bc7773
--- /dev/null
+++ b/RhythmBaseCore/Adofai/Events/ADCameraRelativeTo.cs
@@ -0,0 +1,12 @@
+using System;
+namespace RhythmBase.Adofai.Events
+{
+ public enum ADCameraRelativeTo
+ {
+ Player,
+ Tile,
+ Global,
+ LastPosition,
+ LastPositionNoRotation
+ }
+}
diff --git a/RhythmBaseCore/Adofai/Events/ADCheckpoint.cs b/RhythmBaseCore/Adofai/Events/ADCheckpoint.cs
new file mode 100644
index 0000000..0de90a0
--- /dev/null
+++ b/RhythmBaseCore/Adofai/Events/ADCheckpoint.cs
@@ -0,0 +1,15 @@
+using System;
+namespace RhythmBase.Adofai.Events
+{
+ public class ADCheckpoint : ADBaseTileEvent
+ {
+ public ADCheckpoint()
+ {
+ Type = ADEventType.Checkpoint;
+ }
+
+ public override ADEventType Type { get; }
+
+ public int TileOffset { get; set; }
+ }
+}
diff --git a/RhythmBaseCore/Adofai/Events/ADColorTrack.cs b/RhythmBaseCore/Adofai/Events/ADColorTrack.cs
new file mode 100644
index 0000000..2abd58b
--- /dev/null
+++ b/RhythmBaseCore/Adofai/Events/ADColorTrack.cs
@@ -0,0 +1,34 @@
+using RhythmBase.Components;
+using System;
+namespace RhythmBase.Adofai.Events
+{
+ public class ADColorTrack : ADBaseTileEvent
+ {
+ public ADColorTrack()
+ {
+ Type = ADEventType.ColorTrack;
+ }
+
+ public override ADEventType Type { get; }
+
+ public ADTrackColorTypes TrackColorType { get; set; }
+
+ public RDColor TrackColor { get; set; }
+
+ public RDColor SecondaryTrackColor { get; set; }
+
+ public float TrackColorAnimDuration { get; set; }
+
+ public ADTrackColorPulses TrackColorPulse { get; set; }
+
+ public float TrackPulseLength { get; set; }
+
+ public ADTrackStyles TrackStyle { get; set; }
+
+ public string TrackTexture { get; set; }
+
+ public float TrackTextureScale { get; set; }
+
+ public float TrackGlowIntensity { get; set; }
+ }
+}
diff --git a/RhythmBaseCore/Adofai/Events/ADCustomBackground.cs b/RhythmBaseCore/Adofai/Events/ADCustomBackground.cs
new file mode 100644
index 0000000..0d6d80f
--- /dev/null
+++ b/RhythmBaseCore/Adofai/Events/ADCustomBackground.cs
@@ -0,0 +1,39 @@
+using System;
+using RhythmBase.Components;
+namespace RhythmBase.Adofai.Events
+{
+ public class ADCustomBackground : ADBaseTaggedTileAction
+ {
+ public ADCustomBackground()
+ {
+ Type = ADEventType.CustomBackground;
+ }
+
+ public override ADEventType Type { get; }
+
+ public RDColor Color { get; set; }
+
+ public string BgImage { get; set; }
+
+ public RDColor ImageColor { get; set; }
+
+ public RDPoint Parallax { get; set; }
+
+ public BgDisplayModes BgDisplayMode { get; set; }
+
+ public bool ImageSmoothing { get; set; }
+
+ public bool LockRot { get; set; }
+
+ public bool LoopBG { get; set; }
+
+ public float ScalingRatio { get; set; }
+
+ public enum BgDisplayModes
+ {
+ FitToScreen,
+ Unscaled,
+ Tiled
+ }
+ }
+}
diff --git a/RhythmBaseCore/Adofai/Events/ADCustomEvent.cs b/RhythmBaseCore/Adofai/Events/ADCustomEvent.cs
new file mode 100644
index 0000000..dd56a7e
--- /dev/null
+++ b/RhythmBaseCore/Adofai/Events/ADCustomEvent.cs
@@ -0,0 +1,28 @@
+using System;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+namespace RhythmBase.Adofai.Events
+{
+ public class ADCustomEvent : ADBaseEvent
+ {
+ public ADCustomEvent()
+ {
+ Type = ADEventType.CustomEvent;
+ }
+
+ public override ADEventType Type { get; }
+
+ [JsonIgnore]
+ public string ActureType
+ {
+ get
+ {
+ return Data["eventType"].ToString();
+ }
+ }
+
+ public JObject Data { get; set; }
+
+ public override string ToString() => ActureType;
+ }
+}
diff --git a/RhythmBaseCore/Adofai/Events/ADCustomTileEvent.cs b/RhythmBaseCore/Adofai/Events/ADCustomTileEvent.cs
new file mode 100644
index 0000000..d5ba446
--- /dev/null
+++ b/RhythmBaseCore/Adofai/Events/ADCustomTileEvent.cs
@@ -0,0 +1,28 @@
+using System;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+namespace RhythmBase.Adofai.Events
+{
+ public class ADCustomTileEvent : ADBaseTileEvent
+ {
+ public ADCustomTileEvent()
+ {
+ Type = ADEventType.CustomTileEvent;
+ }
+
+ public override ADEventType Type { get; }
+
+ [JsonIgnore]
+ public string ActureType
+ {
+ get
+ {
+ return Data["eventType"].ToString();
+ }
+ }
+
+ public JObject Data { get; set; }
+
+ public override string ToString() => string.Format("{0}({1}): {2}", Parent.Index, Parent.Angle, ActureType);
+ }
+}
diff --git a/RhythmBaseCore/Adofai/Events/ADDecorationRelativeTo.cs b/RhythmBaseCore/Adofai/Events/ADDecorationRelativeTo.cs
new file mode 100644
index 0000000..0aa3092
--- /dev/null
+++ b/RhythmBaseCore/Adofai/Events/ADDecorationRelativeTo.cs
@@ -0,0 +1,15 @@
+using System;
+namespace RhythmBase.Adofai.Events
+{
+ public enum ADDecorationRelativeTo
+ {
+ Tile,
+ Global,
+ RedPlanet,
+ BluePlanet,
+ GreenPlanet,
+ Camera,
+ CameraAspect,
+ LastPosition
+ }
+}
diff --git a/RhythmBaseCore/Adofai/Events/ADEasePartBehaviors.cs b/RhythmBaseCore/Adofai/Events/ADEasePartBehaviors.cs
new file mode 100644
index 0000000..6ae39f8
--- /dev/null
+++ b/RhythmBaseCore/Adofai/Events/ADEasePartBehaviors.cs
@@ -0,0 +1,9 @@
+using System;
+namespace RhythmBase.Adofai.Events
+{
+ public enum ADEasePartBehaviors
+ {
+ Repeat,
+ Mirror
+ }
+}
diff --git a/RhythmBaseCore/Adofai/Events/ADEditorComment.cs b/RhythmBaseCore/Adofai/Events/ADEditorComment.cs
new file mode 100644
index 0000000..2fedaf2
--- /dev/null
+++ b/RhythmBaseCore/Adofai/Events/ADEditorComment.cs
@@ -0,0 +1,15 @@
+using System;
+namespace RhythmBase.Adofai.Events
+{
+ public class ADEditorComment : ADBaseTileEvent
+ {
+ public ADEditorComment()
+ {
+ Type = ADEventType.EditorComment;
+ }
+
+ public override ADEventType Type { get; }
+
+ public string Comment { get; set; }
+ }
+}
diff --git a/RhythmBaseCore/Adofai/Events/ADEventType.cs b/RhythmBaseCore/Adofai/Events/ADEventType.cs
new file mode 100644
index 0000000..bd44502
--- /dev/null
+++ b/RhythmBaseCore/Adofai/Events/ADEventType.cs
@@ -0,0 +1,52 @@
+using System;
+namespace RhythmBase.Adofai.Events
+{
+ public enum ADEventType
+ {
+ AddDecoration,
+ AddObject,
+ AddText,
+ AnimateTrack,
+ AutoPlayTiles,
+ Bloom,
+ Bookmark,
+ Checkpoint,
+ ColorTrack,
+ CustomBackground,
+ CustomEvent,
+ CustomTileEvent,
+ EditorComment,
+ Flash,
+ FreeRoam,
+ FreeRoamRemove,
+ FreeRoamTwirl,
+ HallOfMirrors,
+ Hide,
+ Hold,
+ MoveCamera,
+ MoveDecorations,
+ MoveTrack,
+ MultiPlanet,
+ Pause,
+ PlaySound,
+ PositionTrack,
+ RecolorTrack,
+ RepeatEvents,
+ ScaleMargin,
+ ScalePlanets,
+ ScaleRadius,
+ ScreenScroll,
+ ScreenTile,
+ SetConditionalEvents,
+ SetDefaultText,
+ SetFilter,
+ SetHitsound,
+ SetHoldSound,
+ SetObject,
+ SetPlanetRotation,
+ SetSpeed,
+ SetText,
+ ShakeScreen,
+ Twirl
+ }
+}
diff --git a/RhythmBaseCore/Adofai/Events/ADFlash.cs b/RhythmBaseCore/Adofai/Events/ADFlash.cs
new file mode 100644
index 0000000..e0ab6c3
--- /dev/null
+++ b/RhythmBaseCore/Adofai/Events/ADFlash.cs
@@ -0,0 +1,30 @@
+using System;
+using RhythmBase.Components;
+using RhythmBase.Components.Easing;
+using RhythmBase.Events;
+namespace RhythmBase.Adofai.Events
+{
+ public class ADFlash : ADBaseTaggedTileAction, IEaseEvent
+ {
+ public ADFlash()
+ {
+ Type = ADEventType.Flash;
+ }
+
+ public override ADEventType Type { get; }
+
+ public float Duration { get; set; }
+
+ public string Plane { get; set; }
+
+ public RDColor StartColor { get; set; }
+
+ public float StartOpacity { get; set; }
+
+ public RDColor EndColor { get; set; }
+
+ public float EndOpacity { get; set; }
+
+ public EaseType Ease { get; set; }
+ }
+}
diff --git a/RhythmBaseCore/Adofai/Events/ADFreeRoam.cs b/RhythmBaseCore/Adofai/Events/ADFreeRoam.cs
new file mode 100644
index 0000000..d711305
--- /dev/null
+++ b/RhythmBaseCore/Adofai/Events/ADFreeRoam.cs
@@ -0,0 +1,26 @@
+using System;
+using Newtonsoft.Json;
+using RhythmBase.Components;
+using RhythmBase.Components.Easing;
+using RhythmBase.Events;
+namespace RhythmBase.Adofai.Events
+{
+ public class ADFreeRoam : ADBaseTileEvent, IEaseEvent
+ {
+ public ADFreeRoam()
+ {
+ Type = ADEventType.FreeRoam;
+ }
+ public override ADEventType Type { get; }
+ public float Duration { get; set; }
+ public int Size { get; set; }
+ public int PositionOffset { get; set; }
+ public int OutTime { get; set; }
+ [JsonProperty("OutEase")]
+ public EaseType Ease { get; set; }
+ public string HitsoundOnBeats { get; set; }
+ public string HitsoundOffBeats { get; set; }
+ public int CountdownTicks { get; set; }
+ public int AngleCorrectionDir { get; set; }
+ }
+}
diff --git a/RhythmBaseCore/Adofai/Events/ADFreeRoamRemove.cs b/RhythmBaseCore/Adofai/Events/ADFreeRoamRemove.cs
new file mode 100644
index 0000000..68b949a
--- /dev/null
+++ b/RhythmBaseCore/Adofai/Events/ADFreeRoamRemove.cs
@@ -0,0 +1,12 @@
+using System;
+namespace RhythmBase.Adofai.Events
+{
+ public class ADFreeRoamRemove : ADBaseTileEvent
+ {
+ public override ADEventType Type { get; }
+
+ public int Position { get; set; }
+
+ public int Size { get; set; }
+ }
+}
diff --git a/RhythmBaseCore/Adofai/Events/ADFreeRoamTwirl.cs b/RhythmBaseCore/Adofai/Events/ADFreeRoamTwirl.cs
new file mode 100644
index 0000000..8860b59
--- /dev/null
+++ b/RhythmBaseCore/Adofai/Events/ADFreeRoamTwirl.cs
@@ -0,0 +1,15 @@
+using System;
+namespace RhythmBase.Adofai.Events
+{
+ public class ADFreeRoamTwirl : ADBaseTileEvent
+ {
+ public ADFreeRoamTwirl()
+ {
+ Type = ADEventType.FreeRoamTwirl;
+ }
+
+ public override ADEventType Type { get; }
+
+ public int Position { get; set; }
+ }
+}
diff --git a/RhythmBaseCore/Adofai/Events/ADHallOfMirrors.cs b/RhythmBaseCore/Adofai/Events/ADHallOfMirrors.cs
new file mode 100644
index 0000000..a651266
--- /dev/null
+++ b/RhythmBaseCore/Adofai/Events/ADHallOfMirrors.cs
@@ -0,0 +1,15 @@
+using System;
+namespace RhythmBase.Adofai.Events
+{
+ public class ADHallOfMirrors : ADBaseTaggedTileAction
+ {
+ public ADHallOfMirrors()
+ {
+ Type = ADEventType.HallOfMirrors;
+ }
+
+ public override ADEventType Type { get; }
+
+ public bool Enabled { get; set; }
+ }
+}
diff --git a/RhythmBaseCore/Adofai/Events/ADHide.cs b/RhythmBaseCore/Adofai/Events/ADHide.cs
new file mode 100644
index 0000000..f7a769b
--- /dev/null
+++ b/RhythmBaseCore/Adofai/Events/ADHide.cs
@@ -0,0 +1,17 @@
+using System;
+namespace RhythmBase.Adofai.Events
+{
+ public class ADHide : ADBaseTileEvent
+ {
+ public ADHide()
+ {
+ Type = ADEventType.Hide;
+ }
+
+ public override ADEventType Type { get; }
+
+ public bool HideJudgment { get; set; }
+
+ public bool HideTileIcon { get; set; }
+ }
+}
diff --git a/RhythmBaseCore/Adofai/Events/ADHold.cs b/RhythmBaseCore/Adofai/Events/ADHold.cs
new file mode 100644
index 0000000..65621e3
--- /dev/null
+++ b/RhythmBaseCore/Adofai/Events/ADHold.cs
@@ -0,0 +1,19 @@
+using System;
+namespace RhythmBase.Adofai.Events
+{
+ public class ADHold : ADBaseTileEvent
+ {
+ public ADHold()
+ {
+ Type = ADEventType.Hold;
+ }
+
+ public override ADEventType Type { get; }
+
+ public int Duration { get; set; }
+
+ public int DistanceMultiplier { get; set; }
+
+ public bool LandingAnimation { get; set; }
+ }
+}
diff --git a/RhythmBaseCore/Adofai/Events/ADMoveCamera.cs b/RhythmBaseCore/Adofai/Events/ADMoveCamera.cs
new file mode 100644
index 0000000..cd2cbe8
--- /dev/null
+++ b/RhythmBaseCore/Adofai/Events/ADMoveCamera.cs
@@ -0,0 +1,34 @@
+using System;
+using Newtonsoft.Json;
+using RhythmBase.Components;
+using RhythmBase.Components.Easing;
+using RhythmBase.Events;
+namespace RhythmBase.Adofai.Events
+{
+ [JsonObject(ItemNullValueHandling = NullValueHandling.Ignore)]
+ public class ADMoveCamera : ADBaseTaggedTileAction, IEaseEvent
+ {
+ public ADMoveCamera()
+ {
+ Type = ADEventType.MoveCamera;
+ }
+
+ public override ADEventType Type { get; }
+
+ public float Duration { get; set; }
+
+ public EaseType Ease { get; set; }
+
+ public bool DontDisable { get; set; }
+
+ public bool MinVfxOnly { get; set; }
+
+ public ADCameraRelativeTo RelativeTo { get; set; }
+
+ public RDPoint? Position { get; set; }
+
+ public float Rotation { get; set; }
+
+ public float Zoom { get; set; }
+ }
+}
diff --git a/RhythmBaseCore/Adofai/Events/ADMoveDecorations.cs b/RhythmBaseCore/Adofai/Events/ADMoveDecorations.cs
new file mode 100644
index 0000000..065b1aa
--- /dev/null
+++ b/RhythmBaseCore/Adofai/Events/ADMoveDecorations.cs
@@ -0,0 +1,64 @@
+using System;
+using Newtonsoft.Json;
+using RhythmBase.Components;
+using RhythmBase.Components.Easing;
+using RhythmBase.Events;
+namespace RhythmBase.Adofai.Events
+{
+ [JsonObject(ItemNullValueHandling = NullValueHandling.Ignore)]
+ public class ADMoveDecorations : ADBaseTaggedTileAction, IEaseEvent
+ {
+ public ADMoveDecorations()
+ {
+ Type = ADEventType.MoveDecorations;
+ }
+
+ public override ADEventType Type { get; }
+
+ public float Duration { get; set; }
+
+ public string Tag { get; set; }
+
+ public EaseType Ease { get; set; }
+
+ public RDPoint? PositionOffset { get; set; }
+
+ public RDPoint? ParallaxOffset { get; set; }
+
+ public bool? Visible { get; set; }
+
+ public ADDecorationRelativeTo? RelativeTo { get; set; }
+
+ public string DecorationImage { get; set; }
+
+ public RDSize? PivotOffset { get; set; }
+
+ public float? RotationOffset { get; set; }
+
+ public RDSize? Scale { get; set; }
+
+ public RDColor? Color { get; set; }
+
+ public float? Opacity { get; set; }
+
+ public int? Depth { get; set; }
+
+ public RDPoint? Parallax { get; set; }
+
+ public MaskingTypes? MaskingType { get; set; }
+
+ public bool? UseMaskingDepth { get; set; }
+
+ public int? MaskingFrontDepth { get; set; }
+
+ public int? MaskingBackDepth { get; set; }
+
+ public enum MaskingTypes
+ {
+ None,
+ Mask,
+ VisibleInsideMask,
+ VisibleOutsideMask
+ }
+ }
+}
diff --git a/RhythmBaseCore/Adofai/Events/ADMoveTrack.cs b/RhythmBaseCore/Adofai/Events/ADMoveTrack.cs
new file mode 100644
index 0000000..0d36e18
--- /dev/null
+++ b/RhythmBaseCore/Adofai/Events/ADMoveTrack.cs
@@ -0,0 +1,55 @@
+using System;
+using System.Runtime.CompilerServices;
+using RhythmBase.Components;
+using RhythmBase.Components.Easing;
+using RhythmBase.Events;
+namespace RhythmBase.Adofai.Events
+{
+ public class ADMoveTrack : ADBaseTaggedTileAction, IEaseEvent
+ {
+ public ADMoveTrack()
+ {
+ Type = ADEventType.MoveTrack;
+ }
+
+ public override ADEventType Type { get; }
+
+ public object StartTile
+ {
+ [CompilerGenerated]
+ get
+ {
+ return StartTile;
+ }
+ [CompilerGenerated]
+ set
+ {
+ StartTile = RuntimeHelpers.GetObjectValue(value);
+ }
+ }
+
+ public object EndTile
+ {
+ [CompilerGenerated]
+ get
+ {
+ return EndTile;
+ }
+ [CompilerGenerated]
+ set
+ {
+ EndTile = RuntimeHelpers.GetObjectValue(value);
+ }
+ }
+
+ public int GapLength { get; set; }
+
+ public float Duration { get; set; }
+
+ public RDPoint PositionOffset { get; set; }
+
+ public EaseType Ease { get; set; }
+
+ public bool MaxVfxOnly { get; set; }
+ }
+}
diff --git a/RhythmBaseCore/Adofai/Events/ADMultiPlanet.cs b/RhythmBaseCore/Adofai/Events/ADMultiPlanet.cs
new file mode 100644
index 0000000..b122ba6
--- /dev/null
+++ b/RhythmBaseCore/Adofai/Events/ADMultiPlanet.cs
@@ -0,0 +1,15 @@
+using System;
+namespace RhythmBase.Adofai.Events
+{
+ public class ADMultiPlanet : ADBaseTileEvent
+ {
+ public ADMultiPlanet()
+ {
+ Type = ADEventType.MultiPlanet;
+ }
+
+ public override ADEventType Type { get; }
+
+ public string Planets { get; set; }
+ }
+}
diff --git a/RhythmBaseCore/Adofai/Events/ADPause.cs b/RhythmBaseCore/Adofai/Events/ADPause.cs
new file mode 100644
index 0000000..eaad1ab
--- /dev/null
+++ b/RhythmBaseCore/Adofai/Events/ADPause.cs
@@ -0,0 +1,19 @@
+using System;
+namespace RhythmBase.Adofai.Events
+{
+ public class ADPause : ADBaseTileEvent
+ {
+ public ADPause()
+ {
+ Type = ADEventType.Pause;
+ }
+
+ public override ADEventType Type { get; }
+
+ public float Duration { get; set; }
+
+ public int CountdownTicks { get; set; }
+
+ public int AngleCorrectionDir { get; set; }
+ }
+}
diff --git a/RhythmBaseCore/Adofai/Events/ADPlaySound.cs b/RhythmBaseCore/Adofai/Events/ADPlaySound.cs
new file mode 100644
index 0000000..4b316aa
--- /dev/null
+++ b/RhythmBaseCore/Adofai/Events/ADPlaySound.cs
@@ -0,0 +1,17 @@
+using System;
+namespace RhythmBase.Adofai.Events
+{
+ public class ADPlaySound : ADBaseTaggedTileAction
+ {
+ public ADPlaySound()
+ {
+ Type = ADEventType.PlaySound;
+ }
+
+ public override ADEventType Type { get; }
+
+ public string Hitsound { get; set; }
+
+ public int HitsoundVolume { get; set; }
+ }
+}
diff --git a/RhythmBaseCore/Adofai/Events/ADPositionTrack.cs b/RhythmBaseCore/Adofai/Events/ADPositionTrack.cs
new file mode 100644
index 0000000..e01ad03
--- /dev/null
+++ b/RhythmBaseCore/Adofai/Events/ADPositionTrack.cs
@@ -0,0 +1,45 @@
+using System;
+using System.Runtime.CompilerServices;
+using Newtonsoft.Json;
+using RhythmBase.Components;
+namespace RhythmBase.Adofai.Events
+{
+ public class ADPositionTrack : ADBaseTileEvent
+ {
+ public ADPositionTrack()
+ {
+ Type = ADEventType.PositionTrack;
+ }
+
+ public override ADEventType Type { get; }
+
+ public RDPoint PositionOffset { get; set; }
+
+ public object RelativeTo
+ {
+ [CompilerGenerated]
+ get
+ {
+ return RelativeTo;
+ }
+ [CompilerGenerated]
+ set
+ {
+ RelativeTo = RuntimeHelpers.GetObjectValue(value);
+ }
+ }
+
+ public float Rotation { get; set; }
+
+ public float Scale { get; set; }
+
+ public float Opacity { get; set; }
+
+ public bool JustThisTile { get; set; }
+
+ public bool SditorOnly { get; set; }
+
+ [JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
+ public bool? StickToFloors { get; set; }
+ }
+}
diff --git a/RhythmBaseCore/Adofai/Events/ADRecolorTrack.cs b/RhythmBaseCore/Adofai/Events/ADRecolorTrack.cs
new file mode 100644
index 0000000..cefd737
--- /dev/null
+++ b/RhythmBaseCore/Adofai/Events/ADRecolorTrack.cs
@@ -0,0 +1,67 @@
+using System;
+using System.Runtime.CompilerServices;
+using RhythmBase.Components;
+using RhythmBase.Components.Easing;
+using RhythmBase.Events;
+namespace RhythmBase.Adofai.Events
+{
+ public class ADRecolorTrack : ADBaseTaggedTileAction, IEaseEvent
+ {
+ public ADRecolorTrack()
+ {
+ Type = ADEventType.RecolorTrack;
+ }
+
+ public override ADEventType Type { get; }
+
+ public object StartTile
+ {
+ [CompilerGenerated]
+ get
+ {
+ return StartTile;
+ }
+ [CompilerGenerated]
+ set
+ {
+ StartTile = RuntimeHelpers.GetObjectValue(value);
+ }
+ }
+
+ public object EndTile
+ {
+ [CompilerGenerated]
+ get
+ {
+ return EndTile;
+ }
+ [CompilerGenerated]
+ set
+ {
+ EndTile = RuntimeHelpers.GetObjectValue(value);
+ }
+ }
+
+ public int GapLength { get; set; }
+
+ public float Duration { get; set; }
+
+ public ADTrackColorTypes TrackColorType { get; set; }
+
+ public RDColor TrackColor { get; set; }
+
+ public RDColor SecondaryTrackColor { get; set; }
+
+ public float TrackColorAnimDuration { get; set; }
+
+ public ADTrackColorPulses TrackColorPulse { get; set; }
+
+ public float TrackPulseLength { get; set; }
+
+ public ADTrackStyles TrackStyle { get; set; }
+
+ public float TrackGlowIntensity { get; set; }
+
+ public EaseType Ease { get; set; }
+ }
+}
diff --git a/RhythmBaseCore/Adofai/Events/ADRepeatEvents.cs b/RhythmBaseCore/Adofai/Events/ADRepeatEvents.cs
new file mode 100644
index 0000000..480afb7
--- /dev/null
+++ b/RhythmBaseCore/Adofai/Events/ADRepeatEvents.cs
@@ -0,0 +1,31 @@
+using System;
+namespace RhythmBase.Adofai.Events
+{
+ public class ADRepeatEvents : ADBaseTileEvent
+ {
+ public ADRepeatEvents()
+ {
+ Type = ADEventType.RepeatEvents;
+ }
+
+ public override ADEventType Type { get; }
+
+ public RepeatTypes RepeatType { get; set; }
+
+ public int Repetitions { get; set; }
+
+ public int FloorCount { get; set; }
+
+ public float Interval { get; set; }
+
+ public bool ExecuteOnCurrentFloor { get; set; }
+
+ public string Tag { get; set; }
+
+ public enum RepeatTypes
+ {
+ Beat,
+ Floor
+ }
+ }
+}
diff --git a/RhythmBaseCore/Adofai/Events/ADScaleMargin.cs b/RhythmBaseCore/Adofai/Events/ADScaleMargin.cs
new file mode 100644
index 0000000..b0388b9
--- /dev/null
+++ b/RhythmBaseCore/Adofai/Events/ADScaleMargin.cs
@@ -0,0 +1,15 @@
+using System;
+namespace RhythmBase.Adofai.Events
+{
+ public class ADScaleMargin : ADBaseTileEvent
+ {
+ public ADScaleMargin()
+ {
+ Type = ADEventType.ScaleMargin;
+ }
+
+ public override ADEventType Type { get; }
+
+ public int Scale { get; set; }
+ }
+}
diff --git a/RhythmBaseCore/Adofai/Events/ADScalePlanets.cs b/RhythmBaseCore/Adofai/Events/ADScalePlanets.cs
new file mode 100644
index 0000000..ee8cd07
--- /dev/null
+++ b/RhythmBaseCore/Adofai/Events/ADScalePlanets.cs
@@ -0,0 +1,31 @@
+using RhythmBase.Components.Easing;
+using RhythmBase.Events;
+namespace RhythmBase.Adofai.Events
+{
+ public class ADScalePlanets : ADBaseTaggedTileAction, IEaseEvent
+ {
+ public ADScalePlanets()
+ {
+ Type = ADEventType.ScalePlanets;
+ }
+
+ public override ADEventType Type { get; }
+
+ public float Duration { get; set; }
+
+ public TargetPlanets TargetPlanet { get; set; }
+
+ [EaseProperty]
+ public int Scale { get; set; }
+
+ public EaseType Ease { get; set; }
+
+ public enum TargetPlanets
+ {
+ FirePlanet,
+ IcePlanet,
+ GreenPlanet,
+ All
+ }
+ }
+}
diff --git a/RhythmBaseCore/Adofai/Events/ADScaleRadius.cs b/RhythmBaseCore/Adofai/Events/ADScaleRadius.cs
new file mode 100644
index 0000000..6e91fc8
--- /dev/null
+++ b/RhythmBaseCore/Adofai/Events/ADScaleRadius.cs
@@ -0,0 +1,15 @@
+using System;
+namespace RhythmBase.Adofai.Events
+{
+ public class ADScaleRadius : ADBaseTileEvent
+ {
+ public ADScaleRadius()
+ {
+ Type = ADEventType.ScaleRadius;
+ }
+
+ public override ADEventType Type { get; }
+
+ public int Scale { get; set; }
+ }
+}
diff --git a/RhythmBaseCore/Adofai/Events/ADScreenScroll.cs b/RhythmBaseCore/Adofai/Events/ADScreenScroll.cs
new file mode 100644
index 0000000..1828fb6
--- /dev/null
+++ b/RhythmBaseCore/Adofai/Events/ADScreenScroll.cs
@@ -0,0 +1,16 @@
+using System;
+using RhythmBase.Components;
+namespace RhythmBase.Adofai.Events
+{
+ public class ADScreenScroll : ADBaseTaggedTileAction
+ {
+ public ADScreenScroll()
+ {
+ Type = ADEventType.ScreenScroll;
+ }
+
+ public override ADEventType Type { get; }
+
+ public RDSizeN Scroll { get; set; }
+ }
+}
diff --git a/RhythmBaseCore/Adofai/Events/ADScreenTile.cs b/RhythmBaseCore/Adofai/Events/ADScreenTile.cs
new file mode 100644
index 0000000..cf77dd5
--- /dev/null
+++ b/RhythmBaseCore/Adofai/Events/ADScreenTile.cs
@@ -0,0 +1,22 @@
+using System;
+using RhythmBase.Components;
+using RhythmBase.Components.Easing;
+using RhythmBase.Events;
+namespace RhythmBase.Adofai.Events
+{
+ public class ADScreenTile : ADBaseTaggedTileAction, IEaseEvent
+ {
+ public ADScreenTile()
+ {
+ Type = ADEventType.ScreenTile;
+ }
+
+ public override ADEventType Type { get; }
+
+ public float Duration { get; set; }
+
+ public RDPoint Tile { get; set; }
+
+ public EaseType Ease { get; set; }
+ }
+}
diff --git a/RhythmBaseCore/Adofai/Events/ADSetConditionalEvents.cs b/RhythmBaseCore/Adofai/Events/ADSetConditionalEvents.cs
new file mode 100644
index 0000000..0921373
--- /dev/null
+++ b/RhythmBaseCore/Adofai/Events/ADSetConditionalEvents.cs
@@ -0,0 +1,37 @@
+using System;
+namespace RhythmBase.Adofai.Events
+{
+ public class ADSetConditionalEvents : ADBaseTileEvent
+ {
+ public ADSetConditionalEvents()
+ {
+ Type = ADEventType.SetConditionalEvents;
+ }
+
+ public override ADEventType Type { get; }
+
+ public string PerfectTag { get; set; }
+
+ public string HitTag { get; set; }
+
+ public string EarlyPerfectTag { get; set; }
+
+ public string LatePerfectTag { get; set; }
+
+ public string BarelyTag { get; set; }
+
+ public string VeryEarlyTag { get; set; }
+
+ public string VeryLateTag { get; set; }
+
+ public string MissTag { get; set; }
+
+ public string TooEarlyTag { get; set; }
+
+ public string TooLateTag { get; set; }
+
+ public string LossTag { get; set; }
+
+ public string OnCheckpointTag { get; set; }
+ }
+}
diff --git a/RhythmBaseCore/Adofai/Events/ADSetDefaultText.cs b/RhythmBaseCore/Adofai/Events/ADSetDefaultText.cs
new file mode 100644
index 0000000..c1bd8da
--- /dev/null
+++ b/RhythmBaseCore/Adofai/Events/ADSetDefaultText.cs
@@ -0,0 +1,34 @@
+using System;
+using Newtonsoft.Json;
+using RhythmBase.Components;
+using RhythmBase.Components.Easing;
+using RhythmBase.Events;
+namespace RhythmBase.Adofai.Events
+{
+ [JsonObject(ItemNullValueHandling = NullValueHandling.Ignore)]
+ public class ADSetDefaultText : ADBaseTaggedTileAction, IEaseEvent
+ {
+ public ADSetDefaultText()
+ {
+ Type = ADEventType.SetDefaultText;
+ }
+
+ public override ADEventType Type { get; }
+
+ public float Duration { get; set; }
+
+ public EaseType Ease { get; set; }
+
+ public RDColor? DefaultTextColor { get; set; }
+
+ public RDColor? DefaultTextShadowColor { get; set; }
+
+ public RDPoint? LevelTitlePosition { get; set; }
+
+ public string LevelTitleText { get; set; }
+
+ public string CongratsText { get; set; }
+
+ public string PerfectText { get; set; }
+ }
+}
diff --git a/RhythmBaseCore/Adofai/Events/ADSetFilter.cs b/RhythmBaseCore/Adofai/Events/ADSetFilter.cs
new file mode 100644
index 0000000..ccd187e
--- /dev/null
+++ b/RhythmBaseCore/Adofai/Events/ADSetFilter.cs
@@ -0,0 +1,73 @@
+using System;
+using RhythmBase.Components;
+using RhythmBase.Components.Easing;
+using RhythmBase.Events;
+namespace RhythmBase.Adofai.Events
+{
+ public class ADSetFilter : ADBaseTaggedTileAction, IEaseEvent
+ {
+ public ADSetFilter()
+ {
+ Type = ADEventType.SetFilter;
+ }
+
+ public override ADEventType Type { get; }
+
+ public Filters Filter { get; set; }
+
+ public string Enabled { get; set; }
+
+ public int Intensity { get; set; }
+
+ public float Duration { get; set; }
+
+ public EaseType Ease { get; set; }
+
+ public string DisableOthers { get; set; }
+
+ public enum Filters
+ {
+ Grayscale,
+ Sepia,
+ Invert,
+ VHS,
+ EightiesTV,
+ FiftiesTV,
+ Arcade,
+ LED,
+ Rain,
+ Blizzard,
+ PixelSnow,
+ Compression,
+ Glitch,
+ Pixelate,
+ Waves,
+ Static,
+ Grain,
+ MotionBlur,
+ Fisheye,
+ Aberration,
+ Drawing,
+ Neon,
+ Handheld,
+ NightVision,
+ Funk,
+ Tunnel,
+ Weird3D,
+ Blur,
+ BlurFocus,
+ GaussianBlur,
+ HexagonBlack,
+ Posterize,
+ Sharpen,
+ Contrast,
+ EdgeBlackLine,
+ OilPaint,
+ SuperDot,
+ WaterDrop,
+ LightWater,
+ Petals,
+ PetalsInstant
+ }
+ }
+}
diff --git a/RhythmBaseCore/Adofai/Events/ADSetHitsound.cs b/RhythmBaseCore/Adofai/Events/ADSetHitsound.cs
new file mode 100644
index 0000000..9774ed5
--- /dev/null
+++ b/RhythmBaseCore/Adofai/Events/ADSetHitsound.cs
@@ -0,0 +1,25 @@
+using System;
+namespace RhythmBase.Adofai.Events
+{
+ public class ADSetHitsound : ADBaseTileEvent
+ {
+ public ADSetHitsound()
+ {
+ Type = ADEventType.SetHitsound;
+ }
+
+ public override ADEventType Type { get; }
+
+ public string GameSound { get; set; }
+
+ public string Hitsound { get; set; }
+
+ public int HitsoundVolume { get; set; }
+
+ public enum GameSounds
+ {
+ Hitsound,
+ Midspin
+ }
+ }
+}
diff --git a/RhythmBaseCore/Adofai/Events/ADSetHoldSound.cs b/RhythmBaseCore/Adofai/Events/ADSetHoldSound.cs
new file mode 100644
index 0000000..80e612b
--- /dev/null
+++ b/RhythmBaseCore/Adofai/Events/ADSetHoldSound.cs
@@ -0,0 +1,29 @@
+using System;
+namespace RhythmBase.Adofai.Events
+{
+ public class ADSetHoldSound : ADBaseTileEvent
+ {
+ public ADSetHoldSound()
+ {
+ Type = ADEventType.SetHoldSound;
+ }
+
+ public override ADEventType Type { get; }
+
+ public string HoldStartSound { get; set; }
+
+ public string HoldLoopSound { get; set; }
+
+ public string HoldEndSound { get; set; }
+
+ public string HoldMidSound { get; set; }
+
+ public string HoldMidSoundType { get; set; }
+
+ public float HoldMidSoundDelay { get; set; }
+
+ public string HoldMidSoundTimingRelativeTo { get; set; }
+
+ public int HoldSoundVolume { get; set; }
+ }
+}
diff --git a/RhythmBaseCore/Adofai/Events/ADSetObject.cs b/RhythmBaseCore/Adofai/Events/ADSetObject.cs
new file mode 100644
index 0000000..60667ea
--- /dev/null
+++ b/RhythmBaseCore/Adofai/Events/ADSetObject.cs
@@ -0,0 +1,34 @@
+using Newtonsoft.Json;
+using RhythmBase.Components;
+using RhythmBase.Components.Easing;
+using RhythmBase.Events;
+namespace RhythmBase.Adofai.Events
+{
+ [JsonObject(ItemNullValueHandling = NullValueHandling.Ignore)]
+ public class ADSetObject : ADBaseTaggedTileAction, IEaseEvent
+ {
+ public ADSetObject()
+ {
+ Type = ADEventType.SetObject;
+ }
+ public override ADEventType Type { get; }
+ public float Duration { get; set; }
+ public string Tag { get; set; }
+ public EaseType Ease { get; set; }
+ public RDColor? PlanetColor { get; set; }
+ public RDColor? PlanetTailColor { get; set; }
+ public float? TrackAngle { get; set; }
+ public ADTrackColorTypes? TrackColorType { get; set; }
+ public RDColor? TrackColor { get; set; }
+ public RDColor? SecondaryTrackColor { get; set; }
+ public float? TrackColorAnimDuration { get; set; }
+ public float? TrackOpacity { get; set; }
+ public ADTrackStyles? TrackStyle { get; set; }
+ public string TrackIcon { get; set; }
+ public float? TrackIconAngle { get; set; }
+ public bool? TrackRedSwirl { get; set; }
+ public bool? TrackGraySetSpeedIcon { get; set; }
+ public bool? TrackGlowEnabled { get; set; }
+ public RDColor? TrackGlowColor { get; set; }
+ }
+}
diff --git a/RhythmBaseCore/Adofai/Events/ADSetPlanetRotation.cs b/RhythmBaseCore/Adofai/Events/ADSetPlanetRotation.cs
new file mode 100644
index 0000000..42e9822
--- /dev/null
+++ b/RhythmBaseCore/Adofai/Events/ADSetPlanetRotation.cs
@@ -0,0 +1,19 @@
+using System;
+namespace RhythmBase.Adofai.Events
+{
+ public class ADSetPlanetRotation : ADBaseTileEvent
+ {
+ public ADSetPlanetRotation()
+ {
+ Type = ADEventType.SetPlanetRotation;
+ }
+
+ public override ADEventType Type { get; }
+
+ public string Ease { get; set; }
+
+ public int EaseParts { get; set; }
+
+ public ADEasePartBehaviors EasePartBehavior { get; set; }
+ }
+}
diff --git a/RhythmBaseCore/Adofai/Events/ADSetSpeed.cs b/RhythmBaseCore/Adofai/Events/ADSetSpeed.cs
new file mode 100644
index 0000000..09215f2
--- /dev/null
+++ b/RhythmBaseCore/Adofai/Events/ADSetSpeed.cs
@@ -0,0 +1,25 @@
+using System;
+namespace RhythmBase.Adofai.Events
+{
+ public class ADSetSpeed : ADBaseTaggedTileAction
+ {
+ public ADSetSpeed()
+ {
+ Type = ADEventType.SetSpeed;
+ }
+
+ public override ADEventType Type { get; }
+
+ public SpeedTypes SpeedType { get; set; }
+
+ public float BeatsPerMinute { get; set; }
+
+ public float BpmMultiplier { get; set; }
+
+ public enum SpeedTypes
+ {
+ Bpm,
+ Multiplier
+ }
+ }
+}
diff --git a/RhythmBaseCore/Adofai/Events/ADSetText.cs b/RhythmBaseCore/Adofai/Events/ADSetText.cs
new file mode 100644
index 0000000..058c7c9
--- /dev/null
+++ b/RhythmBaseCore/Adofai/Events/ADSetText.cs
@@ -0,0 +1,17 @@
+using System;
+namespace RhythmBase.Adofai.Events
+{
+ public class ADSetText : ADBaseTaggedTileAction
+ {
+ public ADSetText()
+ {
+ Type = ADEventType.SetText;
+ }
+
+ public override ADEventType Type { get; }
+
+ public string DecText { get; set; }
+
+ public string Tag { get; set; }
+ }
+}
diff --git a/RhythmBaseCore/Adofai/Events/ADShakeScreen.cs b/RhythmBaseCore/Adofai/Events/ADShakeScreen.cs
new file mode 100644
index 0000000..62d3ab9
--- /dev/null
+++ b/RhythmBaseCore/Adofai/Events/ADShakeScreen.cs
@@ -0,0 +1,26 @@
+using System;
+using RhythmBase.Components;
+using RhythmBase.Components.Easing;
+using RhythmBase.Events;
+namespace RhythmBase.Adofai.Events
+{
+ public class ADShakeScreen : ADBaseTaggedTileAction, IEaseEvent
+ {
+ public ADShakeScreen()
+ {
+ Type = ADEventType.ShakeScreen;
+ }
+
+ public override ADEventType Type { get; }
+
+ public float Duration { get; set; }
+
+ public float Strength { get; set; }
+
+ public float Intensity { get; set; }
+
+ public EaseType Ease { get; set; }
+
+ public float FadeOut { get; set; }
+ }
+}
diff --git a/RhythmBaseCore/Adofai/Events/ADTile.cs b/RhythmBaseCore/Adofai/Events/ADTile.cs
new file mode 100644
index 0000000..93de4df
--- /dev/null
+++ b/RhythmBaseCore/Adofai/Events/ADTile.cs
@@ -0,0 +1,35 @@
+using Newtonsoft.Json;
+using RhythmBase.Adofai.Components;
+namespace RhythmBase.Adofai.Events
+{
+ public class ADTile : ADTypedList
+ {
+ public float Angle
+ {
+ get => _angle;
+ set => _angle = value is > (-360f) and < 360f ? ((value + 540f) % 360f) - 180f : -999f;
+ }
+ public bool IsMidSpin => _angle < -180f || _angle > 180f;
+ public ADBeat Beat => new(Parent?.Calculator!, Index + (_angle / 180f));
+ [JsonIgnore]
+ public ADLevel? Parent { get; set; }
+ public ADTile() { }
+ public ADTile(IEnumerable actions)
+ {
+ foreach (ADBaseTileEvent i in actions)
+ {
+ i.Parent = this;
+ Add(i);
+ }
+ }
+ public int Index => Parent?.IndexOf(this) ?? -1;
+ public override string ToString() => string.Format("[{0}]{1}<{2}>{3}",
+ [
+ Index,
+ Beat,
+ IsMidSpin ? "MS".PadRight(4) : _angle.ToString().PadLeft(4),
+ ((IEnumerable)this).Any() ? string.Format(", Count = {0}", ((IEnumerable)this).Count()) : string.Empty
+ ]);
+ private float _angle = 0;
+ }
+}
diff --git a/RhythmBaseCore/Adofai/Events/ADTileRelativeTo.cs b/RhythmBaseCore/Adofai/Events/ADTileRelativeTo.cs
new file mode 100644
index 0000000..19ceeca
--- /dev/null
+++ b/RhythmBaseCore/Adofai/Events/ADTileRelativeTo.cs
@@ -0,0 +1,10 @@
+using System;
+namespace RhythmBase.Adofai.Events
+{
+ public enum ADTileRelativeTo
+ {
+ ThisTile,
+ Start,
+ End
+ }
+}
diff --git a/RhythmBaseCore/Adofai/Events/ADTrackColorPulses.cs b/RhythmBaseCore/Adofai/Events/ADTrackColorPulses.cs
new file mode 100644
index 0000000..9783929
--- /dev/null
+++ b/RhythmBaseCore/Adofai/Events/ADTrackColorPulses.cs
@@ -0,0 +1,10 @@
+using System;
+namespace RhythmBase.Adofai.Events
+{
+ public enum ADTrackColorPulses
+ {
+ None,
+ Forward,
+ Backward
+ }
+}
diff --git a/RhythmBaseCore/Adofai/Events/ADTrackColorTypes.cs b/RhythmBaseCore/Adofai/Events/ADTrackColorTypes.cs
new file mode 100644
index 0000000..d5a7fe7
--- /dev/null
+++ b/RhythmBaseCore/Adofai/Events/ADTrackColorTypes.cs
@@ -0,0 +1,14 @@
+using System;
+namespace RhythmBase.Adofai.Events
+{
+ public enum ADTrackColorTypes
+ {
+ Single,
+ Stripes,
+ Glow,
+ Blink,
+ Switch,
+ Rainbow,
+ Volume
+ }
+}
diff --git a/RhythmBaseCore/Adofai/Events/ADTrackStyles.cs b/RhythmBaseCore/Adofai/Events/ADTrackStyles.cs
new file mode 100644
index 0000000..46306d7
--- /dev/null
+++ b/RhythmBaseCore/Adofai/Events/ADTrackStyles.cs
@@ -0,0 +1,13 @@
+using System;
+namespace RhythmBase.Adofai.Events
+{
+ public enum ADTrackStyles
+ {
+ Standard,
+ Neon,
+ NeonLight,
+ Basic,
+ Gems,
+ Minimal
+ }
+}
diff --git a/RhythmBaseCore/Adofai/Events/ADTwirl.cs b/RhythmBaseCore/Adofai/Events/ADTwirl.cs
new file mode 100644
index 0000000..0d52fd7
--- /dev/null
+++ b/RhythmBaseCore/Adofai/Events/ADTwirl.cs
@@ -0,0 +1,13 @@
+using System;
+namespace RhythmBase.Adofai.Events
+{
+ public class ADTwirl : ADBaseTileEvent
+ {
+ public ADTwirl()
+ {
+ Type = ADEventType.Twirl;
+ }
+
+ public override ADEventType Type { get; }
+ }
+}
diff --git a/RhythmBaseCore/Adofai/Utils/ADBeatCalculator.cs b/RhythmBaseCore/Adofai/Utils/ADBeatCalculator.cs
new file mode 100644
index 0000000..440400a
--- /dev/null
+++ b/RhythmBaseCore/Adofai/Utils/ADBeatCalculator.cs
@@ -0,0 +1,37 @@
+using RhythmBase.Adofai.Components;
+using RhythmBase.Adofai.Events;
+using RhythmBase.Extensions;
+namespace RhythmBase.Adofai.Utils
+{
+ ///
+ /// Beat Calculator.
+ ///
+ public class ADBeatCalculator
+ {
+ internal ADBeatCalculator(ADLevel level)
+ {
+ Collection = level;
+ Refresh();
+ }
+ private void Refresh()
+ {
+ _DefaultBpm = Collection.Settings.Bpm;
+ _MidSpins = Collection.Where((i) => i.IsMidSpin).ToList();
+ ////this._SetSpeeds = this.Collection.EventsWhere().ToList();
+ ////this._Twirls = this.Collection.EventsWhere().ToList();
+ ////this._Pauses = this.Collection.EventsWhere().ToList();
+ ////this._Holds = this.Collection.EventsWhere().ToList();
+ ////this._Freeroams = this.Collection.EventsWhere().ToList();
+ }
+#pragma warning disable IDE0052 // 删除未读的私有成员
+ internal ADLevel Collection;
+ private float _DefaultBpm = 100;
+ private List _MidSpins = [];
+ private readonly List _SetSpeeds = [];
+ private readonly List _Twirls = [];
+ private readonly List _Pauses = [];
+ private readonly List _Holds = [];
+ private readonly List _Freeroams = [];
+#pragma warning restore IDE0052 // 删除未读的私有成员
+ }
+}
diff --git a/RhythmBaseCore/Adofai/Utils/Utils.cs b/RhythmBaseCore/Adofai/Utils/Utils.cs
new file mode 100644
index 0000000..18b5fb6
--- /dev/null
+++ b/RhythmBaseCore/Adofai/Utils/Utils.cs
@@ -0,0 +1,154 @@
+using Microsoft.VisualBasic.CompilerServices;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Converters;
+using RhythmBase.Adofai.Components;
+using RhythmBase.Adofai.Converters;
+using RhythmBase.Adofai.Events;
+using RhythmBase.Converters;
+using RhythmBase.Events;
+using RhythmBase.Exceptions;
+using RhythmBase.Settings;
+using System.Collections.ObjectModel;
+namespace RhythmBase.Adofai.Utils
+{
+ ///
+ /// Useful utils.
+ ///
+ [StandardModule]
+ public static class Utils
+ {
+ ///
+ /// Converts a given type to an ADEventType enumeration.
+ ///
+ /// The type to convert.
+ /// The corresponding ADEventType enumeration.
+ /// Thrown when no matching EventType is found or multiple matching EventTypes are found.
+ public static ADEventType ADConvertToEnum(Type type)
+ {
+ if (ADETypesToEnum == null)
+ {
+ if (type.Name.StartsWith("AD"))
+ {
+ string name = type.Name[2..];
+ if (Enum.TryParse(name, out ADEventType result))
+ return result;
+ }
+ throw new IllegalEventTypeException(type, "Unable to find a matching EventType.");
+ }
+ ADEventType ADConvertToEnum;
+ try
+ {
+ ADConvertToEnum = ADETypesToEnum![type].Single();
+ }
+ catch
+ {
+ throw new IllegalEventTypeException(type, "Multiple matching EventTypes were found. Please check if the type is an abstract class type.", new ArgumentException("Multiple matching EventTypes were found. Please check if the type is an abstract class type.", nameof(type)));
+ }
+ return ADConvertToEnum;
+ }
+
+ ///
+ /// Converts a generic type to an ADEventType enumeration.
+ ///
+ /// The type to convert, which must inherit from ADBaseEvent and have a parameterless constructor.
+ /// The corresponding ADEventType enumeration.
+ public static ADEventType ConvertToADEnum() where T : ADBaseEvent, new() => ADConvertToEnum(typeof(T));
+
+ ///
+ /// Converts a generic type to an array of ADEventType enumerations.
+ ///
+ /// The type to convert, which must inherit from BaseEvent.
+ /// An array of corresponding ADEventType enumerations.
+ /// Thrown when no matching EventType is found.
+ public static ADEventType[] ConvertToADEnums() where T : BaseEvent
+ {
+ ADEventType[] ConvertToADEnums;
+ try
+ {
+ ConvertToADEnums = ADETypesToEnum[typeof(T)];
+ }
+ catch
+ {
+ throw new IllegalEventTypeException(typeof(T), "This exception is not expected. Please contact the developer to handle this exception.");
+ }
+ return ConvertToADEnums;
+ }
+
+ ///
+ /// Converts a string representation of an ADEventType to a Type.
+ ///
+ /// The string representation of the ADEventType.
+ /// The corresponding Type.
+ public static Type ADConvertToType(string type)
+ {
+ bool flag = Enum.TryParse(type, out ADEventType result);
+ Type ADConvertToType;
+ if (flag)
+ ADConvertToType = ConvertToType(result);
+ else
+ ADConvertToType = ConvertToType(ADEventType.CustomEvent);
+ return ADConvertToType;
+ }
+
+ ///
+ /// Converts an ADEventType enumeration to a Type.
+ ///
+ /// The ADEventType enumeration to convert.
+ /// The corresponding Type.
+ /// Thrown when the type is illegal.
+ /// Thrown when the value does not exist in the EventType enumeration.
+ public static Type ConvertToType(this ADEventType type)
+ {
+ Type ConvertToType;
+ if (ADEnumToEType == null)
+ ConvertToType = Type.GetType(string.Format("{0}.AD{1}", typeof(ADBaseEvent).Namespace, type))
+ ?? throw new RhythmBaseException(string.Format("Illegal Type: {0}.", type));
+ else
+ try
+ {
+ ConvertToType = ADEnumToEType[type];
+ }
+ catch
+ {
+ throw new IllegalEventTypeException(Conversions.ToString((int)type), "This value does not exist in the EventType enumeration.");
+ }
+ return ConvertToType;
+ }
+
+ ///
+ /// Gets a JsonSerializer configured with the necessary converters for the given ADLevel and settings.
+ ///
+ /// The ADLevel instance.
+ /// The LevelReadOrWriteSettings instance.
+ /// A configured JsonSerializer instance.
+ public static JsonSerializer GetSerializer(this ADLevel adlevel, LevelReadOrWriteSettings settings)
+ {
+ JsonSerializer AllInOneSerializer = new();
+ JsonConverterCollection converters = AllInOneSerializer.Converters;
+ converters.Add(new StringEnumConverter());
+ converters.Add(new ColorConverter());
+ converters.Add(new ADTileConverter(adlevel));
+ converters.Add(new ADCustomTileEventConverter(adlevel, settings));
+ converters.Add(new ADCustomEventConverter(adlevel, settings));
+ converters.Add(new ADBaseTileEventConverter(adlevel, settings));
+ converters.Add(new ADBaseEventConverter(adlevel, settings));
+ return AllInOneSerializer;
+ }
+
+ private static readonly ReadOnlyCollection ADETypes = (from i in typeof(ADBaseEvent).Assembly.GetTypes()
+ where i.IsAssignableTo(typeof(ADBaseEvent))
+ select i).ToList().AsReadOnly();
+
+ ///
+ /// A dictionary that records the correspondence of ADEventType to event types inheriting from ADBaseEvent.
+ ///
+ public static readonly ReadOnlyDictionary ADETypesToEnum = ADETypes.ToDictionary((Type i) => i, (Type i) => (from j in ADETypes
+ where (j == i || j.IsAssignableTo(i)) && !j.IsAbstract
+ select j).Select((Type j) => ADConvertToEnum(j)).ToArray()).AsReadOnly();
+
+ ///
+ /// A dictionary that records the correspondence of event types inheriting from ADBaseEvent to ADEventType.
+ ///
+ public static readonly ReadOnlyDictionary ADEnumToEType = Enum.GetValues().ToDictionary((ADEventType i) => i, ConvertToType).AsReadOnly();
+ }
+}
diff --git a/RhythmBaseCore/Components/BaseConditional.cs b/RhythmBaseCore/Components/BaseConditional.cs
new file mode 100644
index 0000000..650a76c
--- /dev/null
+++ b/RhythmBaseCore/Components/BaseConditional.cs
@@ -0,0 +1,72 @@
+using Newtonsoft.Json;
+namespace RhythmBase.Components
+{
+ ///
+ /// Represents a base class for different types of conditions.
+ ///
+ public abstract class BaseConditional
+ {
+ ///
+ /// Gets the type of this condition.
+ ///
+ public abstract ConditionType Type { get; }
+
+ ///
+ /// Gets or sets the condition tag. Its role has not been clarified.
+ ///
+ public string Tag { get; set; } = "";
+
+ ///
+ /// Gets or sets the condition name.
+ ///
+ public string Name { get; set; } = "";
+
+ ///
+ /// Gets the 1-based serial number of this condition in the parent collection.
+ ///
+ public int Id => checked(ParentCollection.IndexOf(this) + 1);
+
+ ///
+ /// Returns the name of the condition.
+ ///
+ /// The name of the condition.
+ public override string ToString() => Name;
+
+ ///
+ /// Gets or sets the parent collection of conditions.
+ ///
+ [JsonIgnore]
+ internal List ParentCollection = [];
+
+ ///
+ /// Specifies the type of condition.
+ ///
+ public enum ConditionType
+ {
+ ///
+ /// Condition based on the last hit.
+ ///
+ LastHit,
+
+ ///
+ /// Custom condition.
+ ///
+ Custom,
+
+ ///
+ /// Condition based on the number of times executed.
+ ///
+ TimesExecuted,
+
+ ///
+ /// Condition based on the language.
+ ///
+ Language,
+
+ ///
+ /// Condition based on the player mode.
+ ///
+ PlayerMode
+ }
+ }
+}
diff --git a/RhythmBaseCore/Components/Bookmark.cs b/RhythmBaseCore/Components/Bookmark.cs
new file mode 100644
index 0000000..37c2613
--- /dev/null
+++ b/RhythmBaseCore/Components/Bookmark.cs
@@ -0,0 +1,47 @@
+namespace RhythmBase.Components
+{
+ ///
+ /// Represents a bookmark in the rhythm base.
+ ///
+ public class Bookmark
+ {
+ ///
+ /// Gets or sets the beat where the bookmark is located.
+ ///
+ public RDBeat Beat { get; set; }
+
+ ///
+ /// Gets or sets the color of the bookmark.
+ ///
+ public BookmarkColors Color { get; set; }
+
+ ///
+ /// Returns a string that represents the current bookmark.
+ ///
+ /// A string that represents the current bookmark.
+ public override string ToString() => string.Format("{0}, {1}", Beat, Color);
+
+ ///
+ /// Specifies the colors available for bookmarks.
+ ///
+ public enum BookmarkColors
+ {
+ ///
+ /// Represents the color blue.
+ ///
+ Blue,
+ ///
+ /// Represents the color red.
+ ///
+ Red,
+ ///
+ /// Represents the color yellow.
+ ///
+ Yellow,
+ ///
+ /// Represents the color green.
+ ///
+ Green
+ }
+ }
+}
diff --git a/RhythmBaseCore/Components/ClassicBeatStatus.cs b/RhythmBaseCore/Components/ClassicBeatStatus.cs
new file mode 100644
index 0000000..2453e2a
--- /dev/null
+++ b/RhythmBaseCore/Components/ClassicBeatStatus.cs
@@ -0,0 +1,50 @@
+namespace RhythmBase.Components
+{
+ ///
+ /// Represents the status of a classic beat.
+ ///
+ public struct ClassicBeatStatus
+ {
+ ///
+ /// Gets or sets the status type of the beat.
+ ///
+ public StatusType Status;
+
+ ///
+ /// Gets or sets the beat count.
+ ///
+ public ushort BeatCount;
+
+ ///
+ /// Defines the various status types for a classic beat.
+ ///
+ public enum StatusType
+ {
+#pragma warning disable CS1591
+ Unset = -1,
+ None,
+ Synco,
+ Beat_Open,
+ Beat_Flash,
+ Beat_Double_Flash,
+ Beat_Triple_Flash,
+ Beat_Close,
+ X_Open,
+ X_Flash,
+ X_Close,
+ X_Synco_Open,
+ X_Synco_Flash,
+ X_Synco_Close,
+ Up_Open,
+ Up_Close,
+ Down_Open,
+ Down_Close,
+ Swing_Left,
+ Swing_Right,
+ Swing_Bounce,
+ Held_Start,
+ Held_End
+#pragma warning restore CS1591
+ }
+ }
+}
diff --git a/RhythmBaseCore/Components/Condition.cs b/RhythmBaseCore/Components/Condition.cs
new file mode 100644
index 0000000..d5b6bee
--- /dev/null
+++ b/RhythmBaseCore/Components/Condition.cs
@@ -0,0 +1,56 @@
+using Microsoft.VisualBasic.CompilerServices;
+using RhythmBase.Exceptions;
+using System.Text.RegularExpressions;
+namespace RhythmBase.Components
+{
+ ///
+ /// The conditions of the event.
+ ///
+ public class Condition
+ {
+ ///
+ /// Condition list.
+ ///
+ public List<(bool Enabled, BaseConditional Conditional)> ConditionLists;
+
+ ///
+ /// The time of effectiveness of the condition.
+ ///
+ public float Duration { get; set; }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public Condition()
+ {
+ ConditionLists = [];
+ }
+
+ ///
+ /// Loads a condition from a string.
+ ///
+ /// The text to load the condition from.
+ /// A new instance of the class.
+ /// Thrown when the condition is illegal.
+ internal static Condition Load(string text)
+ {
+ Condition @out = new();
+ MatchCollection Matches = Regex.Matches(text, "(~?\\d+)(?=[&d])");
+ if (Matches.Count > 0)
+ {
+ @out.Duration = float.Parse(Regex.Match(text, "[\\d\\.]").Value);
+ return @out;
+ }
+ throw new RhythmBaseException(string.Format("Illegal condition: {0}.", text));
+ }
+
+ ///
+ /// Converts conditions to a string.
+ ///
+ /// A string in the format supported by RDLevel.
+ public string Serialize() => $"{string.Join("&", ConditionLists.Select((i) => (i.Enabled ? "" : "~") + i.Conditional.Id.ToString()))}d{Duration}";
+
+ ///
+ public override string ToString() => Serialize();
+ }
+}
diff --git a/RhythmBaseCore/Components/Conditions/CustomCondition.cs b/RhythmBaseCore/Components/Conditions/CustomCondition.cs
new file mode 100644
index 0000000..76a8181
--- /dev/null
+++ b/RhythmBaseCore/Components/Conditions/CustomCondition.cs
@@ -0,0 +1,28 @@
+namespace RhythmBase.Components.Conditions
+{
+ ///
+ /// Represents a custom condition with an expression.
+ ///
+ public class CustomCondition : BaseConditional
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public CustomCondition()
+ {
+ Type = ConditionType.Custom;
+ }
+
+ ///
+ /// Gets or sets the expression for the custom condition.
+ ///
+ /// The expression as a string.
+ public string Expression { get; set; } = "";
+
+ ///
+ /// Gets the type of the condition.
+ ///
+ /// The type of the condition, which is .
+ public override ConditionType Type { get; }
+ }
+}
diff --git a/RhythmBaseCore/Components/Conditions/LanguageCondition.cs b/RhythmBaseCore/Components/Conditions/LanguageCondition.cs
new file mode 100644
index 0000000..efc0f78
--- /dev/null
+++ b/RhythmBaseCore/Components/Conditions/LanguageCondition.cs
@@ -0,0 +1,83 @@
+using Newtonsoft.Json;
+namespace RhythmBase.Components.Conditions
+{
+ ///
+ /// Represents a condition based on the game language.
+ ///
+ public class LanguageCondition : BaseConditional
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public LanguageCondition()
+ {
+ Type = ConditionType.Language;
+ }
+
+ ///
+ /// Gets or sets the game language.
+ ///
+ [JsonProperty(nameof(Language))]
+ public Languages Language
+ {
+ get;
+ set;
+ }
+
+ ///
+ /// Gets the type of the condition.
+ ///
+ public override ConditionType Type { get; }
+
+ ///
+ /// Represents the supported game languages.
+ ///
+ public enum Languages
+ {
+ ///
+ /// English language.
+ ///
+ English,
+
+ ///
+ /// Spanish language.
+ ///
+ Spanish,
+
+ ///
+ /// Portuguese language.
+ ///
+ Portuguese,
+
+ ///
+ /// Simplified Chinese language.
+ ///
+ ChineseSimplified,
+
+ ///
+ /// Traditional Chinese language.
+ ///
+ ChineseTraditional,
+
+ ///
+ /// Korean language.
+ ///
+ Korean,
+
+ ///
+ /// Polish language.
+ ///
+ Polish,
+
+ ///
+ /// Japanese language.
+ ///
+ Japanese,
+
+ ///
+ /// German language.
+ ///
+ German
+ }
+ }
+}
diff --git a/RhythmBaseCore/Components/Conditions/LastHitCondition.cs b/RhythmBaseCore/Components/Conditions/LastHitCondition.cs
new file mode 100644
index 0000000..4eaab85
--- /dev/null
+++ b/RhythmBaseCore/Components/Conditions/LastHitCondition.cs
@@ -0,0 +1,73 @@
+namespace RhythmBase.Components.Conditions
+{
+ ///
+ /// Represents a condition based on the last hit in a rhythm game.
+ ///
+ public class LastHitCondition : BaseConditional
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public LastHitCondition()
+ {
+ Type = ConditionType.LastHit;
+ }
+
+ ///
+ /// Gets the type of the condition.
+ ///
+ public override ConditionType Type { get; }
+
+ ///
+ /// Gets or sets the row where the last hit occurred.
+ ///
+ public sbyte Row { get; set; }
+
+ ///
+ /// Gets or sets the result that determines under what condition the event will be executed.
+ ///
+ public HitResult Result { get; set; }
+
+ ///
+ /// Defines the possible results of a hit.
+ ///
+ [Flags]
+ public enum HitResult
+ {
+ ///
+ /// The hit was perfect.
+ ///
+ Perfect = 0,
+
+ ///
+ /// The hit was slightly early.
+ ///
+ SlightlyEarly = 2,
+
+ ///
+ /// The hit was slightly late.
+ ///
+ SlightlyLate = 3,
+
+ ///
+ /// The hit was very early.
+ ///
+ VeryEarly = 4,
+
+ ///
+ /// The hit was very late.
+ ///
+ VeryLate = 5,
+
+ ///
+ /// The hit was either slightly early or slightly late.
+ ///
+ AnyEarlyOrLate = 7,
+
+ ///
+ /// The hit was missed.
+ ///
+ Missed = 15
+ }
+ }
+}
diff --git a/RhythmBaseCore/Components/Conditions/PlayerModeCondition.cs b/RhythmBaseCore/Components/Conditions/PlayerModeCondition.cs
new file mode 100644
index 0000000..c40005d
--- /dev/null
+++ b/RhythmBaseCore/Components/Conditions/PlayerModeCondition.cs
@@ -0,0 +1,32 @@
+namespace RhythmBase.Components.Conditions
+{
+ ///
+ /// Represents a condition based on the player mode.
+ ///
+ public class PlayerModeCondition : BaseConditional
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public PlayerModeCondition()
+ {
+ Type = ConditionType.PlayerMode;
+ }
+
+ ///
+ /// Gets or sets a value indicating whether two-player mode is enabled.
+ ///
+ ///
+ /// true if two-player mode is enabled; otherwise, false.
+ ///
+ public bool TwoPlayerMode { get; set; }
+
+ ///
+ /// Gets the type of the condition.
+ ///
+ ///
+ /// The type of the condition.
+ ///
+ public override ConditionType Type { get; }
+ }
+}
diff --git a/RhythmBaseCore/Components/Conditions/TimesExecutedCondition.cs b/RhythmBaseCore/Components/Conditions/TimesExecutedCondition.cs
new file mode 100644
index 0000000..e2188e3
--- /dev/null
+++ b/RhythmBaseCore/Components/Conditions/TimesExecutedCondition.cs
@@ -0,0 +1,26 @@
+namespace RhythmBase.Components.Conditions
+{
+ ///
+ /// Represents a condition based on the number of times it has been executed.
+ ///
+ public class TimesExecutedCondition : BaseConditional
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public TimesExecutedCondition()
+ {
+ Type = ConditionType.TimesExecuted;
+ }
+
+ ///
+ /// Gets or sets the maximum number of executions allowed.
+ ///
+ public int MaxTimes { get; set; }
+
+ ///
+ /// Gets the type of the condition.
+ ///
+ public override ConditionType Type { get; }
+ }
+}
diff --git a/RhythmBaseCore/Components/DecorationEventCollection.cs b/RhythmBaseCore/Components/DecorationEventCollection.cs
new file mode 100644
index 0000000..3388bb9
--- /dev/null
+++ b/RhythmBaseCore/Components/DecorationEventCollection.cs
@@ -0,0 +1,94 @@
+using Microsoft.VisualBasic.CompilerServices;
+using Newtonsoft.Json;
+using RhythmBase.Events;
+using RhythmBase.Exceptions;
+namespace RhythmBase.Components
+{
+ ///
+ /// A decoration.
+ ///
+ [JsonObject]
+ public class DecorationEventCollection : OrderedEventCollection
+ {
+ ///
+ /// Decorated ID.
+ ///
+ [JsonProperty("id")]
+ public string Id
+ {
+ get => _id;
+ set => _id = value;
+ }
+ ///
+ /// Decoration index.
+ ///
+ [JsonProperty("row")]
+ public int Index => Parent?.Decorations.ToList().IndexOf(this) ?? throw new RhythmBaseException();
+
+ ///
+ /// Room.
+ ///
+ [JsonProperty("rooms")]
+ public RDSingleRoom Room { get; set; }
+ ///
+ /// The file reference used by the decoration.
+ ///
+ [JsonProperty("filename")]
+ public string Filename { get; set; } = "";
+ ///
+ /// Decoration depth.
+ ///
+ public int Depth { get; set; }
+ ///
+ /// The filter used for this decoration.
+ ///
+ public Filters Filter { get; set; }
+ ///
+ /// The initial visibility of this decoration.
+ ///
+ public bool Visible { get; set; }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public DecorationEventCollection()
+ {
+ Room = new RDSingleRoom(RDRoomIndex.Room1);
+ }
+ /// Decoration room.
+ internal DecorationEventCollection(RDSingleRoom room)
+ {
+ Room = room;
+ _id = GetHashCode().ToString();
+ }
+ ///
+ /// Add an event to decoration.
+ ///
+ /// Decoration event.
+ public override void Add(BaseDecorationAction item)
+ {
+ item._parent?.Remove(item);
+ item._parent = this;
+ Parent?.Add(item);
+ }
+ internal void AddSafely(BaseDecorationAction item) => base.Add(item);
+ ///
+ /// Remove an event from decoration.
+ ///
+ /// A decoration event.
+ public override bool Remove(BaseDecorationAction item) => Parent?.Remove(item) ?? throw new RhythmBaseException();
+ internal bool RemoveSafely(BaseDecorationAction item) => base.Remove(item);
+ ///
+ public override string ToString() => string.Format("{0}, {1}, {2}, {3}",
+ [
+ _id,
+ Index,
+ Room,
+ Filename
+ ]);
+ internal DecorationEventCollection Clone() => (DecorationEventCollection)MemberwiseClone();
+ private string _id = "";
+ [JsonIgnore]
+ internal RDLevel? Parent = null;
+ }
+}
diff --git a/RhythmBaseCore/Components/Easing/Ease.cs b/RhythmBaseCore/Components/Easing/Ease.cs
new file mode 100644
index 0000000..c0fa924
--- /dev/null
+++ b/RhythmBaseCore/Components/Easing/Ease.cs
@@ -0,0 +1,282 @@
+namespace RhythmBase.Components.Easing
+
+{
+ ///
+ /// The EaseType enumeration represents various types of easing functions.
+ /// These functions are used to create smooth transitions in animations.
+ ///
+ public enum EaseType
+ {
+ ///
+ /// Unset.
+ ///
+ Unset = -1,
+ ///
+ ///Ease Linear.
+ ///
+ Linear,
+ ///
+ ///Ease InSine.
+ ///
+ InSine,
+ ///
+ ///Ease OutSine.
+ ///
+ OutSine,
+ ///
+ ///Ease InOutSine.
+ ///
+ InOutSine,
+ ///
+ ///Ease InQuad.
+ ///
+ InQuad,
+ ///
+ ///Ease OutQuad.
+ ///
+ OutQuad,
+ ///
+ ///Ease InOutQuad.
+ ///
+ InOutQuad,
+ ///
+ ///Ease InCubic.
+ ///
+ InCubic,
+ ///
+ ///Ease OutCubic.
+ ///
+ OutCubic,
+ ///
+ ///Ease InOutCubic.
+ ///
+ InOutCubic,
+ ///
+ ///Ease InQuart.
+ ///
+ InQuart,
+ ///
+ ///Ease OutQuart.
+ ///
+ OutQuart,
+ ///
+ ///Ease InOutQuart.
+ ///
+ InOutQuart,
+ ///
+ ///Ease InQuint.
+ ///
+ InQuint,
+ ///
+ ///Ease OutQuint.
+ ///
+ OutQuint,
+ ///
+ ///Ease InOutQuint.
+ ///
+ InOutQuint,
+ ///
+ ///Ease InExpo.
+ ///
+ InExpo,
+ ///
+ ///Ease OutExpo.
+ ///
+ OutExpo,
+ ///
+ ///Ease InOutExpo.
+ ///
+ InOutExpo,
+ ///
+ ///Ease InCirc.
+ ///
+ InCirc,
+ ///
+ ///Ease OutCirc.
+ ///
+ OutCirc,
+ ///
+ ///Ease InOutCirc.
+ ///
+ InOutCirc,
+ ///
+ ///Ease InElastic.
+ ///
+ InElastic,
+ ///
+ ///Ease OutElastic.
+ ///
+ OutElastic,
+ ///
+ ///Ease InOutElastic.
+ ///
+ InOutElastic,
+ ///
+ ///Ease InBack.
+ ///
+ InBack,
+ ///
+ ///Ease OutBack.
+ ///
+ OutBack,
+ ///
+ ///Ease InOutBack.
+ ///
+ InOutBack,
+ ///
+ ///Ease InBounce.
+ ///
+ InBounce,
+ ///
+ ///Ease OutBounce.
+ ///
+ OutBounce,
+ ///
+ ///Ease InOutBounce.
+ ///
+ InOutBounce,
+ ///
+ ///Ease SmoothStep.
+ ///
+ SmoothStep,
+ }
+ ///
+ /// Ease Calculate module.
+ ///
+ public static class Ease
+ {
+ ///
+ /// Calculates the value with the specified ease type.
+ ///
+ /// Ease type.
+ /// A doubleing-point number in the range of 0 to 1.
+ /// Easing result.
+ public static double Calculate(this EaseType type, double x) => type switch
+ {
+ EaseType.Unset => EaseFunction.None(x),
+ EaseType.Linear => EaseFunction.Linear(x),
+ EaseType.InSine => EaseFunction.InSine(x),
+ EaseType.OutSine => EaseFunction.OutSine(x),
+ EaseType.InOutSine => EaseFunction.InOutSine(x),
+ EaseType.InQuad => EaseFunction.InQuad(x),
+ EaseType.OutQuad => EaseFunction.OutQuad(x),
+ EaseType.InOutQuad => EaseFunction.InOutQuad(x),
+ EaseType.InCubic => EaseFunction.InCubic(x),
+ EaseType.OutCubic => EaseFunction.OutCubic(x),
+ EaseType.InOutCubic => EaseFunction.InOutCubic(x),
+ EaseType.InQuart => EaseFunction.InQuart(x),
+ EaseType.OutQuart => EaseFunction.OutQuart(x),
+ EaseType.InOutQuart => EaseFunction.InOutQuart(x),
+ EaseType.InQuint => EaseFunction.InQuint(x),
+ EaseType.OutQuint => EaseFunction.OutQuint(x),
+ EaseType.InOutQuint => EaseFunction.InOutQuint(x),
+ EaseType.InExpo => EaseFunction.InExpo(x),
+ EaseType.OutExpo => EaseFunction.OutExpo(x),
+ EaseType.InOutExpo => EaseFunction.InOutExpo(x),
+ EaseType.InCirc => EaseFunction.InCirc(x),
+ EaseType.OutCirc => EaseFunction.OutCirc(x),
+ EaseType.InOutCirc => EaseFunction.InOutCirc(x),
+ EaseType.InElastic => EaseFunction.InElastic(x),
+ EaseType.OutElastic => EaseFunction.OutElastic(x),
+ EaseType.InOutElastic => EaseFunction.InOutElastic(x),
+ EaseType.InBack => EaseFunction.InBack(x),
+ EaseType.OutBack => EaseFunction.OutBack(x),
+ EaseType.InOutBack => EaseFunction.InOutBack(x),
+ EaseType.InBounce => EaseFunction.InBounce(x),
+ EaseType.OutBounce => EaseFunction.OutBounce(x),
+ EaseType.InOutBounce => EaseFunction.InOutBounce(x),
+ EaseType.SmoothStep => EaseFunction.SmoothStep(x),
+ _ => 0,
+ };
+ ///
+ /// Calculates the value with the specified ease type.
+ ///
+ /// Ease type.
+ /// A doubleing-point number in the range of 0 to 1.
+ /// The starting value of the easing result.
+ /// The endding value of the easing result
+ /// Easing result.
+ public static double Calculate(this EaseType Type, double x, double from, double to) => Type.Calculate(x) * (to - from) + from;
+ ///
+ /// Ease types.
+ ///
+
+ private static class EaseFunction
+ {
+ private const double c1 = 1.525;
+ private const double c2 = 1.70158;
+ public static double None(double x) => 0;
+ public static double Linear(double x) => x;
+ public static double InSine(double x) => 1 - Math.Cos(x * double.Pi / 2);
+ public static double OutSine(double x) => Math.Sin(x * double.Pi / 2);
+ public static double InOutSine(double x) => -(Math.Cos(x * double.Pi) - 1) / 2;
+ public static double InQuad(double x) => Math.Pow(x, 2);
+ public static double OutQuad(double x) => 1 - Math.Pow(1 - x, 2);
+ public static double InOutQuad(double x) => x < 0.5 ? 2 * Math.Pow(x, 2) : 1 - Math.Pow(-2 * x + 2, 2) / 2;
+ public static double InCubic(double x) => Math.Pow(x, 3);
+ public static double OutCubic(double x) => 1 - Math.Pow(1 - x, 3);
+ public static double InOutCubic(double x) => x < 0.5 ? 4 * Math.Pow(x, 3) : 1 - Math.Pow(-2 * x + 2, 3) / 2;
+ public static double InQuart(double x) => Math.Pow(x, 4);
+ public static double OutQuart(double x) => 1 - Math.Pow(1 - x, 4);
+ public static double InOutQuart(double x) => x < 0.5 ? 8 * Math.Pow(x, 4) : 1 - Math.Pow(-2 * x + 2, 4) / 2;
+ public static double InQuint(double x) => Math.Pow(x, 5);
+ public static double OutQuint(double x) => 1 - Math.Pow(1 - x, 5);
+ public static double InOutQuint(double x) => x < 0.5 ? 16 * Math.Pow(x, 5) : 1 - Math.Pow(-2 * x + 2, 5) / 2;
+ public static double InExpo(double x) => x == 0 ? 0 : Math.Pow(2, 10 * x - 10);
+ public static double OutExpo(double x) => x == 1 ? 1 : 1 - Math.Pow(2, -10 * x);
+ public static double InOutExpo(double x) => x == 0
+ ? 0
+ : x == 1
+ ? 1
+ : x < 0.5 ? Math.Pow(2, 20 * x - 10) / 2
+ : (2 - Math.Pow(2, -20 * x + 10)) / 2;
+ public static double InCirc(double x) => 1 - Math.Sqrt(1 - Math.Pow(x, 2));
+ public static double OutCirc(double x) => Math.Sqrt(1 - Math.Pow(x - 1, 2));
+ public static double InOutCirc(double x) => x < 0.5
+ ? (1 - Math.Sqrt(1 - Math.Pow(2 * x, 2))) / 2
+ : (Math.Sqrt(1 - Math.Pow(-2 * x + 2, 2)) + 1) / 2;
+ public static double InElastic(double x) => x == 0
+ ? 0
+ : x == 1
+ ? 1
+ : -Math.Pow(2, 10 * x - 10) * Math.Sin((x * 10 - 10.75) * (2 * double.Pi / 3));
+ public static double OutElastic(double x) => x == 0
+ ? 0
+ : x == 1
+ ? 1
+ : Math.Pow(2, -10 * x) * Math.Sin((x * 10 - 0.75) * (2 * double.Pi / 3)) + 1;
+ public static double InOutElastic(double x) => x == 0
+ ? 0
+ : x == 1
+ ? 1
+ : x < 0.5
+ ? -(Math.Pow(2, 20 * x - 10) * Math.Sin((20 * x - 11.125) * (2 * double.Pi / 4.5))) / 2
+ : Math.Pow(2, -20 * x + 10) * Math.Sin((20 * x - 11.125) * (2 * double.Pi / 4.5)) / 2 + 1;
+ public static double InBack(double x) => (c2 + 1) * x * x * x - c2 * x * x;
+ public static double OutBack(double x) => 1 + (c2 + 1) * Math.Pow(x - 1, 3) + c2 * Math.Pow(x - 1, 2);
+ public static double InOutBack(double x) => x < 0.5
+ ? Math.Pow(2 * x, 2) * ((c2 * c1 + 1) * 2 * x - c2 * c1) / 2
+ : (Math.Pow(2 * x - 2, 2) * ((c2 * c1 + 1) * (x * 2 - 2) + c2 * c1) + 2) / 2;
+ public static double InBounce(double x) =>
+ 1 - OutBounce(1 - x);
+ public static double OutBounce(double x)
+ {
+ const double n1 = 7.5625;
+ const double d1 = 2.75;
+ return x switch
+ {
+ < 1 / d1 => n1 * x * x,
+ < 2 / d1 => n1 * (x -= 1.5 / d1) * x + 0.75,
+ < 2.5 / d1 => n1 * (x -= 2.25 / d1) * x + 0.9375,
+ _ => n1 * (x -= 2.625 / d1) * x + 0.984375,
+ };
+ }
+ public static double InOutBounce(double x) =>
+ x < 0.5
+ ? (1 - OutBounce(1 - 2 * x)) / 2
+ : (1 + OutBounce(2 * x - 1)) / 2;
+ public static double SmoothStep(double x) =>
+ (3 - 2 * x) * Math.Pow(x, 2);
+ }
+ }
+}
diff --git a/RhythmBaseCore/Components/Easing/EaseNode.cs b/RhythmBaseCore/Components/Easing/EaseNode.cs
new file mode 100644
index 0000000..0b22d8b
--- /dev/null
+++ b/RhythmBaseCore/Components/Easing/EaseNode.cs
@@ -0,0 +1,29 @@
+namespace RhythmBase.Components.Easing
+{
+ ///
+ /// Represents a node in the easing process.
+ ///
+ /// The target value of the easing node.
+ public struct EaseNode(float target)
+ {
+ ///
+ /// Gets or sets the time at which the easing node starts.
+ ///
+ public float Time { get; set; } = 0;
+
+ ///
+ /// Gets or sets the target value of the easing node.
+ ///
+ public float Target { get; set; } = target;
+
+ ///
+ /// Gets or sets the duration of the easing node.
+ ///
+ public float Duration { get; set; } = 0;
+
+ ///
+ /// Gets or sets the type of easing to be applied.
+ ///
+ public EaseType Type { get; set; } = EaseType.Linear;
+ }
+}
diff --git a/RhythmBaseCore/Components/Easing/EasePropertyAttribute.cs b/RhythmBaseCore/Components/Easing/EasePropertyAttribute.cs
new file mode 100644
index 0000000..1f7a13e
--- /dev/null
+++ b/RhythmBaseCore/Components/Easing/EasePropertyAttribute.cs
@@ -0,0 +1,10 @@
+namespace RhythmBase.Components.Easing
+{
+ ///
+ /// An attribute to mark properties for easing functions.
+ ///
+ [AttributeUsage(AttributeTargets.Property)]
+ public class EasePropertyAttribute : Attribute
+ {
+ }
+}
diff --git a/RhythmBaseCore/Components/Easing/EasePropertyColor.cs b/RhythmBaseCore/Components/Easing/EasePropertyColor.cs
new file mode 100644
index 0000000..5f8874e
--- /dev/null
+++ b/RhythmBaseCore/Components/Easing/EasePropertyColor.cs
@@ -0,0 +1,63 @@
+using RhythmBase.Events;
+using System.Reflection;
+
+namespace RhythmBase.Components.Easing
+{
+ ///
+ /// Represents an easing property with a color value.
+ ///
+ public class EasePropertyColor : IEaseProperty
+ {
+ private EaseValue _r;
+ private EaseValue _g;
+ private EaseValue _b;
+ private EaseValue _a;
+ ///
+ public RDColor GetValue(RDBeat beat) => RDColor.FromRgba(
+ (byte)Math.Clamp(_r.GetValue(beat.BeatOnly), 0, 255),
+ (byte)Math.Clamp(_g.GetValue(beat.BeatOnly), 0, 255),
+ (byte)Math.Clamp(_b.GetValue(beat.BeatOnly), 0, 255),
+ (byte)Math.Clamp(_a.GetValue(beat.BeatOnly), 0, 255));
+ ///
+ public static bool CanConvert(object data) => data is RDColor;
+ ///
+ public static EaseNode?[] Convert(IEaseEvent data, PropertyInfo property)
+ {
+ RDColor? value = (RDColor?)property.GetValue(data);
+ return [
+ value?.R is byte vr ? new(vr) {
+ Duration = data.Duration,
+ Time = ((BaseEvent)data).Beat.BeatOnly,
+ Type = data.Ease
+ } : null,
+ value?.G is byte vg ? new(vg) {
+ Duration = data.Duration,
+ Time = ((BaseEvent)data).Beat.BeatOnly,
+ Type = data.Ease
+ } : null,
+ value?.B is byte vb ? new(vb) {
+ Duration = data.Duration,
+ Time = ((BaseEvent)data).Beat.BeatOnly,
+ Type = data.Ease
+ } : null,
+ value?.A is byte va ? new(va) {
+ Duration = data.Duration,
+ Time = ((BaseEvent)data).Beat.BeatOnly,
+ Type = data.Ease
+ } : null,
+ ];
+ }
+ ///
+ public static IEaseProperty CreateEaseProperty(RDColor originalValue, IEaseEvent[] data, PropertyInfo property)
+ {
+ EaseNode?[][] nodes = [.. data.Select(d => Convert(d, property))];
+ return new EasePropertyColor()
+ {
+ _r = new(originalValue.R, [.. nodes.Select(i => i[0]).Where(i => i is not null).Cast()]),
+ _g = new(originalValue.G, [.. nodes.Select(i => i[1]).Where(i => i is not null).Cast()]),
+ _b = new(originalValue.B, [.. nodes.Select(i => i[2]).Where(i => i is not null).Cast()]),
+ _a = new(originalValue.A, [.. nodes.Select(i => i[3]).Where(i => i is not null).Cast()])
+ };
+ }
+ }
+}
diff --git a/RhythmBaseCore/Components/Easing/EasePropertyFloat.cs b/RhythmBaseCore/Components/Easing/EasePropertyFloat.cs
new file mode 100644
index 0000000..80be12c
--- /dev/null
+++ b/RhythmBaseCore/Components/Easing/EasePropertyFloat.cs
@@ -0,0 +1,38 @@
+using RhythmBase.Events;
+using System.Reflection;
+
+namespace RhythmBase.Components.Easing
+{
+ ///
+ /// Represents an easing property with a float value.
+ ///
+ public class EasePropertyFloat : IEaseProperty
+ {
+ private EaseValue _value;
+ ///
+ public float GetValue(RDBeat beat) => _value.GetValue(beat.BeatOnly);
+ ///
+ public static bool CanConvert(object data) => data is float;
+ ///
+ public static EaseNode?[] Convert(IEaseEvent data, PropertyInfo property)
+ {
+ RDExpression? value = (RDExpression?)property.GetValue(data);
+ return [
+ value is RDExpression v? new EaseNode(v.Value) {
+ Duration = data.Duration,
+ Time = ((BaseEvent)data).Beat.BeatOnly,
+ Type = data.Ease
+ } : null,
+ ];
+ }
+ ///
+ public static IEaseProperty CreateEaseProperty(float originalValue, IEaseEvent[] data, PropertyInfo property)
+ {
+ EaseNode?[][] nodes = [.. data.Select(d => Convert(d, property))];
+ return new EasePropertyFloat()
+ {
+ _value = new(originalValue, [.. nodes.Select(i => i[0]).Where(i => i is not null).Cast()])
+ };
+ }
+ }
+}
diff --git a/RhythmBaseCore/Components/Easing/EasePropertyPoint.cs b/RhythmBaseCore/Components/Easing/EasePropertyPoint.cs
new file mode 100644
index 0000000..38a4087
--- /dev/null
+++ b/RhythmBaseCore/Components/Easing/EasePropertyPoint.cs
@@ -0,0 +1,45 @@
+using RhythmBase.Events;
+using System.Reflection;
+
+namespace RhythmBase.Components.Easing
+{
+ ///
+ /// Represents an easing property with a point value.
+ ///
+ public class EasePropertyPoint : IEaseProperty
+ {
+ private EaseValue _x;
+ private EaseValue _y;
+ ///
+ public RDPointN GetValue(RDBeat beat) => new(_x.GetValue(beat.BeatOnly), _y.GetValue(beat.BeatOnly));
+ ///
+ public static bool CanConvert(object data) => data is RDPoint;
+ ///
+ public static EaseNode?[] Convert(IEaseEvent data, PropertyInfo property)
+ {
+ RDPointE? value = (RDPointE?)property.GetValue(data);
+ return [
+ value?.X?.Value is float vx ? new(vx) {
+ Duration = data.Duration,
+ Time = ((BaseEvent)data).Beat.BeatOnly,
+ Type = data.Ease
+ } : null,
+ value?.Y?.Value is float vy ? new(vy) {
+ Duration = data.Duration,
+ Time = ((BaseEvent)data).Beat.BeatOnly,
+ Type = data.Ease
+ } : null,
+ ];
+ }
+ ///
+ public static IEaseProperty CreateEaseProperty(RDPointN originalValue, IEaseEvent[] data, PropertyInfo property)
+ {
+ EaseNode?[][] nodes = [.. data.Select(d => Convert(d, property))];
+ return new EasePropertyPoint()
+ {
+ _x = new(originalValue.X, [.. nodes.Select(i => i[0]).Where(i => i is not null).Cast()]),
+ _y = new(originalValue.Y, [.. nodes.Select(i => i[1]).Where(i => i is not null).Cast()])
+ };
+ }
+ }
+}
diff --git a/RhythmBaseCore/Components/Easing/EasePropertySize.cs b/RhythmBaseCore/Components/Easing/EasePropertySize.cs
new file mode 100644
index 0000000..013f6e9
--- /dev/null
+++ b/RhythmBaseCore/Components/Easing/EasePropertySize.cs
@@ -0,0 +1,45 @@
+using RhythmBase.Events;
+using System.Reflection;
+
+namespace RhythmBase.Components.Easing
+{
+ ///
+ /// Represents an easing property with a size value.
+ ///
+ public class EasePropertySize : IEaseProperty
+ {
+ private EaseValue _width;
+ private EaseValue _height;
+ ///
+ public RDSizeN GetValue(RDBeat beat) => new(_width.GetValue(beat.BeatOnly), _height.GetValue(beat.BeatOnly));
+ ///
+ public static bool CanConvert(object data) => data is RDSize;
+ ///
+ public static EaseNode?[] Convert(IEaseEvent data, PropertyInfo property)
+ {
+ RDSizeE? value = (RDSizeE?)property.GetValue(data);
+ return [
+ value?.Width?.Value is float vx ? new(vx) {
+ Duration = data.Duration,
+ Time = ((BaseEvent)data).Beat.BeatOnly,
+ Type = data.Ease
+ } : null,
+ value?.Height?.Value is float vy ? new(vy) {
+ Duration = data.Duration,
+ Time = ((BaseEvent)data).Beat.BeatOnly,
+ Type = data.Ease
+ } : null,
+ ];
+ }
+ ///
+ public static IEaseProperty CreateEaseProperty(RDSizeN originalValue, IEaseEvent[] data, PropertyInfo property)
+ {
+ EaseNode?[][] nodes = [.. data.Select(d => Convert(d, property))];
+ return new EasePropertySize()
+ {
+ _width = new(originalValue.Width, [.. nodes.Select(i => i[0]).Where(i => i is not null).Cast()]),
+ _height = new(originalValue.Height, [.. nodes.Select(i => i[1]).Where(i => i is not null).Cast()])
+ };
+ }
+ }
+}
diff --git a/RhythmBaseCore/Components/Easing/EaseValue.cs b/RhythmBaseCore/Components/Easing/EaseValue.cs
new file mode 100644
index 0000000..68aa7a4
--- /dev/null
+++ b/RhythmBaseCore/Components/Easing/EaseValue.cs
@@ -0,0 +1,180 @@
+using System.Data;
+
+namespace RhythmBase.Components.Easing
+{
+ ///
+ /// Represents a value that changes over time according to a series of easing nodes.
+ ///
+ ///
+ /// Initializes a new instance of the class with the specified original value and easing nodes.
+ ///
+ /// The original value before any easing is applied.
+ /// The collection of easing nodes that define how the value changes over time.
+ public struct EaseValue(float originalValue, IEnumerable nodes)
+ {
+ ///
+ /// Gets or sets the collection of easing nodes, ordered by their start time.
+ ///
+ public EaseNode[] Nodes { get; set; } = [.. nodes.OrderBy(x => x.Time)];
+ ///
+ /// Gets or sets the original value before any easing is applied.
+ ///
+ public float OriginalValue { get; set; } = originalValue;
+ ///
+ /// Gets the value at the specified time, taking into account the easing nodes.
+ ///
+ /// The time at which to get the value.
+ /// The value at the specified time.
+ public readonly float GetValue(float time)
+ {
+ if (Nodes.Length == 0 || time <= Nodes[0].Time)
+ return OriginalValue;
+ int i = Nodes.Length - 1;
+ while (i >= 0 && time <= Nodes[i].Time)
+ i--;
+ if (Nodes[i].Time + Nodes[i].Duration <= time)
+ return Nodes[i].Target;
+ Stack ns = new([Nodes[i]]);
+ while (--i > -1)
+ {
+ if (IsInRange(Nodes[i], ns.Peek().Time))
+ {
+ ns.Push(Nodes[i]);
+ }
+ }
+ float origin = i == -1 ? OriginalValue : Nodes[i + 1].Target;
+ while (ns.Count > 1)
+ {
+ EaseNode node = ns.Pop();
+ origin = GetValue(node, ns.Peek().Time, origin);
+ }
+ origin = GetValue(ns.Pop(), time, origin);
+ return origin;
+ }
+ ///
+ /// Determines whether the specified time is within the range of the easing node.
+ ///
+ /// The easing node to check.
+ /// The time to check.
+ /// true if the time is within the range of the easing node; otherwise, false.
+ private static bool IsInRange(EaseNode node, float time) => node.Time <= time && time <= node.Time + node.Duration;
+ ///
+ /// Calculates the value of the easing node at the specified time.
+ ///
+ /// The easing node to calculate the value for.
+ /// The time at which to calculate the value.
+ /// The original value before any easing is applied.
+ /// The calculated value at the specified time.
+ private static float GetValue(EaseNode node, float time, float origin) => node.Duration == 0 ? node.Target : (float)node.Type.Calculate((time - node.Time) / node.Duration, origin, node.Target);
+ ///
+ /// Fits the data points to an easing function with the specified precision.
+ ///
+ /// The original value before any easing is applied.
+ /// The array of time and value pairs to fit.
+ /// The precision for fitting the data points.
+ /// An instance that fits the data points.
+ public static EaseValue Fit(float originalValue, (float time, float value)[] values, float precision = 3f) => Fit(originalValue, values, eases, precision);
+ ///
+ /// Fits the data points to an easing function with the specified precision.
+ ///
+ /// The original value before any easing is applied.
+ /// The array of time and value pairs to fit.
+ /// The precision for fitting the data points.
+ /// The array of easing types to consider.
+ /// An instance that fits the data points.
+ public static EaseValue Fit(float originalValue, (float time, float value)[] values, EaseType[] easeTypes, float precision = 3f)
+ {
+ values = [.. values.OrderBy(x => x.time)];
+ if (values.Length == 0)
+ return new EaseValue(originalValue, []);
+ if (values.Length == 1)
+ return new EaseValue(originalValue, [new EaseNode(values[0].value) { Duration = values[0].time, Type = EaseType.InOutSine }]);
+ if (values.Length == 2)
+ return new EaseValue(originalValue, [new EaseNode(values[0].value) { Time = values[0].time, Duration = values[1].time, Type = EaseType.InOutSine }]);
+ return Fit([(0, originalValue), .. values], easeTypes, precision);
+ }
+ ///
+ /// Fits the data points to an easing function with the specified precision.
+ ///
+ /// The array of time and value pairs to fit.
+ /// The precision for fitting the data points.
+ /// An instance that fits the data points.
+ public static EaseValue Fit((float time, float value)[] values, float precision = 3f) => Fit(values, eases, precision);
+ ///
+ /// Fits the data points to an easing function with the specified precision and ease types.
+ ///
+ /// The array of time and value pairs to fit.
+ /// The array of easing types to consider.
+ /// The precision for fitting the data points.
+ /// Special thanks to mfgujhgh for the algorithm!
+ /// An instance that fits the data points.
+ public static EaseValue Fit((float time, float value)[] values, EaseType[] easeTypes, float precision = 3f)
+ {
+ if (easeTypes.Length == 0)
+ throw new ArgumentException("At least one ease type must be specified.", nameof(easeTypes));
+ values = [.. values.OrderBy(x => x.time)];
+ int[] steps = new int[values.Length],
+ p = new int[values.Length];
+ EaseType[] pe = new EaseType[values.Length];
+ steps[0] = 0;
+ for (int i = 1; i < values.Length; i++)
+ {
+ steps[i] = steps[i - 1] + 1;
+ p[i] = i - 1;
+ pe[i] = easeTypes[0];
+ for (int j = 0; j < i - 1; j++)
+ {
+ if (steps[j] + 1 >= steps[i])
+ continue;
+ EaseType besttype = easeTypes[0];
+ float bests = float.MaxValue;
+ foreach (var e in easeTypes)
+ {
+ float s = Check(values, j, i, e, precision);
+ if (s < bests)
+ {
+ bests = s;
+ besttype = e;
+ }
+ }
+ if (bests < precision && steps[i] > steps[j] + 1)
+ {
+ steps[i] = steps[j] + 1;
+ p[i] = j;
+ pe[i] = besttype;
+ }
+ }
+ }
+ EaseNode[] ns = new EaseNode[steps[^1]];
+ int k = values.Length - 1;
+ while (k != 0)
+ {
+ ns[--steps[k]] = new EaseNode(values[k].value)
+ {
+ Time = values[p[k]].time,
+ Duration = values[k].time - values[p[k]].time,
+ Type = pe[k]
+ };
+ k = p[k];
+ }
+ return new EaseValue(values[0].value, ns);
+ }
+ private static float Check((float time, float value)[] data, int start, int end, EaseType type, float precision)
+ {
+ float s = 0;
+ for (int i = start + 1; i <= end; i++)
+ {
+ float v = data[start].value
+ + (float)type.Calculate((data[i].time - data[start].time) / (data[end].time - data[start].time))
+ * (data[end].value - data[start].value);
+ s = Math.Max(s, Math.Abs(v - data[i].value));
+ if (s > precision)
+ {
+ return s;
+ }
+ }
+ return s;
+ }
+ private static readonly EaseType[] eases = Enum.GetValues();
+ }
+}
diff --git a/RhythmBaseCore/Components/Easing/IEaseProperty.cs b/RhythmBaseCore/Components/Easing/IEaseProperty.cs
new file mode 100644
index 0000000..4e874ff
--- /dev/null
+++ b/RhythmBaseCore/Components/Easing/IEaseProperty.cs
@@ -0,0 +1,54 @@
+using RhythmBase.Events;
+using System.Reflection;
+
+namespace RhythmBase.Components.Easing
+{
+ ///
+ /// Represents an easing property.
+ ///
+ public interface IEaseProperty
+ {
+ }
+ ///
+ /// Represents an easing property with a specific value type.
+ ///
+ /// The type of the value.
+ public interface IEaseProperty : IEaseProperty where TValue : new()
+ {
+ ///
+ /// Gets the type of the value.
+ ///
+ static Type Type => typeof(TValue);
+
+ ///
+ /// Gets the value at the specified beat.
+ ///
+ /// The beat at which to get the value.
+ /// The value at the specified beat.
+ abstract TValue GetValue(RDBeat beat);
+
+ ///
+ /// Determines whether the specified data can be converted to the value type.
+ ///
+ /// The data to check.
+ /// true if the data can be converted; otherwise, false.
+ static abstract bool CanConvert(object data);
+
+ ///
+ /// Converts the specified easing event data to an array of easing nodes.
+ ///
+ /// The easing event data to convert.
+ /// The property information of the easing event.
+ /// An array of easing nodes.
+ static abstract EaseNode?[] Convert(IEaseEvent data, PropertyInfo property);
+
+ ///
+ /// Creates an easing property with the specified original value and easing event data.
+ ///
+ /// The original value before any easing is applied.
+ /// The array of easing event data.
+ /// The property information of the easing event.
+ /// An easing property instance.
+ static abstract IEaseProperty CreateEaseProperty(TValue originalValue, IEaseEvent[] data, PropertyInfo property);
+ }
+}
diff --git a/RhythmBaseCore/Components/Filters.cs b/RhythmBaseCore/Components/Filters.cs
new file mode 100644
index 0000000..392f9f7
--- /dev/null
+++ b/RhythmBaseCore/Components/Filters.cs
@@ -0,0 +1,18 @@
+namespace RhythmBase.Components
+{
+ ///
+ /// Specifies the type of render filter to be used.
+ ///
+ public enum Filters
+ {
+ ///
+ /// Nearest neighbor filtering.
+ ///
+ NearestNeighbor,
+
+ ///
+ /// Bilinear filtering.
+ ///
+ BiliNear
+ }
+}
diff --git a/RhythmBaseCore/Components/IRDVortex.cs b/RhythmBaseCore/Components/IRDVortex.cs
new file mode 100644
index 0000000..77d167c
--- /dev/null
+++ b/RhythmBaseCore/Components/IRDVortex.cs
@@ -0,0 +1,27 @@
+using System.Numerics;
+
+namespace RhythmBase.Components
+{
+ ///
+ /// Represents a vortex interface that supports various mathematical operations.
+ ///
+ /// The type that implements this interface.
+ /// The type of the right operand in addition and subtraction operations.
+ /// The type of the value in multiplication and division operations.
+ public interface IRDVortex :
+ IEquatable,
+ IAdditionOperators,
+ ISubtractionOperators,
+ IMultiplyOperators,
+ IDivisionOperators,
+ IEqualityOperators
+ where TSelf :
+ IEquatable,
+ IAdditionOperators,
+ ISubtractionOperators,
+ IMultiplyOperators,
+ IDivisionOperators,
+ IEqualityOperators
+ {
+ }
+}
diff --git a/RhythmBaseCore/Components/OrderedEventCollection.2.cs b/RhythmBaseCore/Components/OrderedEventCollection.2.cs
new file mode 100644
index 0000000..2f03909
--- /dev/null
+++ b/RhythmBaseCore/Components/OrderedEventCollection.2.cs
@@ -0,0 +1,51 @@
+using RhythmBase.Events;
+namespace RhythmBase.Components
+{
+ ///
+ /// Represents a collection of ordered events.
+ ///
+ /// The type of event.
+ public class OrderedEventCollection : OrderedEventCollection, ICollection where TEvent : IBaseEvent
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public OrderedEventCollection()
+ {
+ }
+ ///
+ /// Initializes a new instance of the class with the specified items.
+ ///
+ /// The items to add to the collection.
+ public OrderedEventCollection(IEnumerable items)
+ {
+ foreach (TEvent item in items)
+ Add(item);
+ }
+ ///
+ /// Concatenates all events in the collection.
+ ///
+ /// An that contains all events in the collection.
+ public new IEnumerable ConcatAll() => eventsBeatOrder.SelectMany(i => i.Value).Cast();
+ ///
+ /// Adds an event to the collection.
+ ///
+ /// The event to add.
+ public virtual void Add(TEvent item) => Add((IBaseEvent)(object)item);
+ ///
+ public virtual bool Contains(TEvent item) => Contains((IBaseEvent)(object)item);
+ ///
+ public void CopyTo(TEvent[] array, int arrayIndex) => CopyTo((IBaseEvent[])(object)array, arrayIndex);
+ ///
+ public virtual bool Remove(TEvent item) => Remove((BaseEvent)(object)item);
+ ///
+ public override string ToString() => string.Format("Count = {0}", Count);
+ ///
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ foreach (KeyValuePair> pair in eventsBeatOrder)
+ foreach (TEvent item in pair.Value.Select(v => (TEvent)v))
+ yield return item;
+ }
+ }
+}
diff --git a/RhythmBaseCore/Components/OrderedEventCollection.cs b/RhythmBaseCore/Components/OrderedEventCollection.cs
new file mode 100644
index 0000000..a382417
--- /dev/null
+++ b/RhythmBaseCore/Components/OrderedEventCollection.cs
@@ -0,0 +1,164 @@
+using Newtonsoft.Json;
+using RhythmBase.Events;
+using System.Collections;
+namespace RhythmBase.Components
+{
+ ///
+ /// A collection of events that maintains the sequence of events.
+ ///
+ public abstract class OrderedEventCollection : ICollection
+ {
+ ///
+ /// Gets the total count of events in the collection.
+ ///
+ [JsonIgnore]
+ public virtual int Count => eventsBeatOrder.Sum((i) => i.Value.Count());
+
+ ///
+ /// Gets a value indicating whether the collection is read-only.
+ ///
+ [JsonIgnore]
+ public bool IsReadOnly { get; }
+
+ ///
+ /// Returns the beat of the last event.
+ ///
+ /// The beat of the last event.
+ [JsonIgnore]
+ public RDBeat Length => eventsBeatOrder.LastOrDefault().Value.FirstOrDefault()?.Beat??new();
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public OrderedEventCollection()
+ {
+ eventsBeatOrder = [];
+ IsReadOnly = false;
+ }
+
+ ///
+ /// Initializes a new instance of the class with the specified items.
+ ///
+ /// The items to add to the collection.
+ public OrderedEventCollection(IEnumerable items)
+ {
+ eventsBeatOrder = [];
+ IsReadOnly = false;
+ foreach (IBaseEvent item in items)
+ Add(item);
+ }
+
+ ///
+ /// Concatenates all events in the collection.
+ ///
+ /// A list of all events in the collection.
+ public IEnumerable ConcatAll() => eventsBeatOrder.SelectMany(i => i.Value).ToList();
+
+ ///
+ /// Adds an event to the collection.
+ ///
+ /// The event to add.
+ public void Add(IBaseEvent item)
+ {
+ TypedEventCollection list = [];
+ if (eventsBeatOrder.TryGetValue(item.Beat, out TypedEventCollection? value))
+ list = value;
+ else
+ eventsBeatOrder.Add(item.Beat, list);
+ list.Add(item);
+ }
+
+ ///
+ /// Clears all events from the collection.
+ ///
+ public void Clear() => eventsBeatOrder.Clear();
+
+ ///
+ /// Determines whether the collection contains a specific event.
+ ///
+ /// The event to locate in the collection.
+ /// true if the event is found in the collection; otherwise, false.
+ public virtual bool Contains(IBaseEvent item) => eventsBeatOrder.ContainsKey(item.Beat) && eventsBeatOrder[item.Beat].Contains(item);
+
+ ///
+ /// Copies the elements of the collection to an array, starting at a particular array index.
+ ///
+ /// The array to copy the elements to.
+ /// The zero-based index in the array at which copying begins.
+ public void CopyTo(IBaseEvent[] array, int arrayIndex)
+ {
+ ArgumentNullException.ThrowIfNull(array);
+ if (arrayIndex < 0 || arrayIndex > array.Length)
+ throw new ArgumentOutOfRangeException(nameof(arrayIndex));
+ if (array.Length - arrayIndex < Count)
+ throw new ArgumentException("The number of elements in the source collection is greater than the available space from arrayIndex to the end of the destination array.");
+
+ foreach (var pair in eventsBeatOrder)
+ {
+ foreach (var item in pair.Value)
+ {
+ array[arrayIndex++] = item;
+ }
+ }
+ }
+ ///
+ /// Removes the first occurrence of a specific event from the collection.
+ ///
+ /// The event to remove from the collection.
+ /// true if the event was successfully removed; otherwise, false.
+ internal bool Remove(IBaseEvent item)
+ {
+ bool Remove;
+ if (Contains(item))
+ {
+ bool result = eventsBeatOrder[item.Beat].Remove(item);
+ if (!eventsBeatOrder[item.Beat].Any())
+ eventsBeatOrder.Remove(item.Beat);
+ Remove = result;
+ }
+ else
+ Remove = false;
+ return Remove;
+ }
+
+ ///
+ /// Returns an enumerator that iterates through the collection.
+ ///
+ /// An enumerator for the collection.
+ public IEnumerator GetEnumerator()
+ {
+ foreach (KeyValuePair> pair in eventsBeatOrder)
+ foreach (IBaseEvent item in pair.Value)
+ yield return item;
+ }
+
+ ///
+ /// Returns an enumerator that iterates through the collection.
+ ///
+ /// An enumerator for the collection.
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ foreach (KeyValuePair> pair in eventsBeatOrder)
+ foreach (IBaseEvent item in pair.Value)
+ yield return item;
+ }
+
+ ///
+ /// Returns a string that represents the current object.
+ ///
+ /// A string that represents the current object.
+ public override string ToString() => string.Format("Count = {0}", Count);
+
+ ///
+ /// Removes the first occurrence of a specific event from the collection.
+ ///
+ /// The event to remove from the collection.
+ /// true if the event was successfully removed; otherwise, false.
+ bool ICollection.Remove(IBaseEvent item) => throw new NotImplementedException();
+
+ ///
+ /// The dictionary that maintains the order of events based on their beats.
+ ///
+ internal SortedDictionary> eventsBeatOrder;
+ }
+}
diff --git a/RhythmBaseCore/Components/PaletteColor.cs b/RhythmBaseCore/Components/PaletteColor.cs
new file mode 100644
index 0000000..e43fef5
--- /dev/null
+++ b/RhythmBaseCore/Components/PaletteColor.cs
@@ -0,0 +1,84 @@
+namespace RhythmBase.Components
+{
+ ///
+ /// Palette color
+ ///
+ ///
+ ///
+ ///
+ /// Specifies whether this object supports alpha channel.
+ public class PaletteColor(bool enableAlpha)
+ {
+ ///
+ /// Get or set a custom color.
+ ///
+ public RDColor? Color
+ {
+ get
+ {
+ RDColor? Color = new RDColor?(EnablePanel ? parent[_panel] : default);
+ return Color;
+ }
+ set
+ {
+ _panel = -1;
+ _color = EnableAlpha
+ ? value
+ : value == null
+ ? null
+ : new RDColor?(value.GetValueOrDefault().WithAlpha(byte.MaxValue));
+ }
+ }
+ ///
+ /// Go back to or set the palette color index.
+ ///
+ public int PaletteIndex
+ {
+ get => _panel;
+ set
+ {
+ if (value >= 0)
+ {
+ _color = null;
+ _panel = value;
+ }
+ }
+ }
+ ///
+ /// Specifies whether this object supports alpha channel.
+ ///
+ ///
+ public bool EnableAlpha { get; } = enableAlpha;
+ ///
+ /// Specifies whether this object is used for this color.
+ ///
+ public bool EnablePanel
+ {
+ get
+ {
+ return PaletteIndex >= 0;
+ }
+ }
+ ///
+ /// The actual color of this object.
+ /// If comes from a palette, it's a palette color.
+ /// If not, it's a custom color.
+ ///
+ public RDColor Value
+ {
+ get
+ {
+ return (EnablePanel ? (parent[_panel]) : _color ?? throw new RhythmBase.Exceptions.RhythmBaseException());
+ }
+ }
+
+ ///
+ public override string ToString() => EnablePanel ? string.Format("{0}: {1}", _panel, Value) : Value.ToString();
+
+ private int _panel;
+
+ private RDColor? _color;
+
+ internal RDColor[] parent = [];
+ }
+}
diff --git a/RhythmBaseCore/Components/RDAudio.cs b/RhythmBaseCore/Components/RDAudio.cs
new file mode 100644
index 0000000..c9c95e5
--- /dev/null
+++ b/RhythmBaseCore/Components/RDAudio.cs
@@ -0,0 +1,65 @@
+using Newtonsoft.Json;
+using RhythmBase.Converters;
+namespace RhythmBase.Components;
+
+///
+/// Represents an audio file with properties for volume, pitch, pan, and offset.
+///
+public class RDAudio
+{
+ ///
+ /// Initializes a new instance of the class with default values.
+ ///
+ public RDAudio() { }
+
+ ///
+ /// Gets or sets the file name of the audio.
+ ///
+ public string Filename { get; set; } = "";
+
+ ///
+ /// Gets or sets the volume of the audio.
+ ///
+ [JsonProperty(DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate)]
+ public int Volume { get; set; } = 100;
+
+ ///
+ /// Gets or sets the pitch of the audio.
+ ///
+ [JsonProperty(DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate)]
+ public int Pitch { get; set; } = 100;
+
+ ///
+ /// Gets or sets the pan of the audio.
+ ///
+ [JsonProperty(DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate)]
+ public int Pan { get; set; }
+
+ ///
+ /// Gets or sets the offset of the audio.
+ ///
+ [JsonProperty(DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate)]
+ [JsonConverter(typeof(MilliSecondConverter))]
+ public TimeSpan Offset { get; set; }
+
+ ///
+ /// Gets a value indicating whether the file is a valid audio file based on its extension.
+ ///
+ [JsonIgnore]
+ public bool IsFile => sourceArray.Contains(Path.GetExtension(Filename));
+
+ private static readonly string[] sourceArray =
+ [
+ ".mp3",
+ ".wav",
+ ".ogg",
+ ".aif",
+ ".aiff"
+ ];
+
+ ///
+ /// Returns a string that represents the current object.
+ ///
+ /// A string that represents the current object.
+ public override string ToString() => Filename;
+}
diff --git a/RhythmBaseCore/Components/RDBeat.cs b/RhythmBaseCore/Components/RDBeat.cs
new file mode 100644
index 0000000..f018933
--- /dev/null
+++ b/RhythmBaseCore/Components/RDBeat.cs
@@ -0,0 +1,445 @@
+using RhythmBase.Exceptions;
+using RhythmBase.Utils;
+using System.Diagnostics.CodeAnalysis;
+using System.Numerics;
+namespace RhythmBase.Components
+{
+ ///
+ /// A beat.
+ ///
+ public struct RDBeat : IComparable, IEquatable, IComparisonOperators
+ {
+ internal readonly RDLevel? BaseLevel => _calculator?.Collection;
+ ///
+ /// Whether this beat cannot be calculated.
+ ///
+ [MemberNotNullWhen(false, nameof(_calculator))]
+ public readonly bool IsEmpty => _calculator == null || (!_isBeatLoaded && !_isBarBeatLoaded && !_isTimeSpanLoaded);
+ ///
+ /// The total number of beats from this moment to the beginning of the level.
+ ///
+ public float BeatOnly
+ {
+ get
+ {
+ IfNullThrowException();
+ if (!_isBeatLoaded)
+ {
+ if (_isBarBeatLoaded)
+ _beat = _calculator!.BarBeatToBeatOnly(_BarBeat.Bar, _BarBeat.Beat) - 1f;
+ else if (_isTimeSpanLoaded)
+ _beat = _calculator!.TimeSpanToBeatOnly(_TimeSpan) - 1f;
+ _isBeatLoaded = true;
+ }
+ return _beat + 1f;
+ }
+ }
+ ///
+ /// The actual bar and beat of this moment.
+ ///
+ public (uint bar, float beat) BarBeat
+ {
+ get
+ {
+ IfNullThrowException();
+ if (!_isBarBeatLoaded)
+ {
+ if (_isBeatLoaded)
+ _BarBeat = _calculator!.BeatOnlyToBarBeat(_beat + 1f);
+ else if (_isTimeSpanLoaded)
+ {
+ _beat = _calculator!.TimeSpanToBeatOnly(_TimeSpan) - 1f;
+ _isBeatLoaded = true;
+ _BarBeat = _calculator.BeatOnlyToBarBeat(_beat + 1f);
+ }
+ _isBarBeatLoaded = true;
+ }
+ return _BarBeat;
+ }
+ }
+ ///
+ /// The total amount of time from the beginning of the level to this beat.
+ ///
+ public TimeSpan TimeSpan
+ {
+ get
+ {
+ IfNullThrowException();
+ if (!_isTimeSpanLoaded)
+ {
+ if (_isBeatLoaded)
+ _TimeSpan = _calculator!.BeatOnlyToTimeSpan(_beat + 1f);
+ else
+ {
+ if (_isBarBeatLoaded)
+ {
+ _beat = _calculator!.BarBeatToBeatOnly(_BarBeat.Bar, _BarBeat.Beat) - 1f;
+ _isBeatLoaded = true;
+ _TimeSpan = _calculator.BeatOnlyToTimeSpan(_beat + 1f);
+ }
+ }
+ _isTimeSpanLoaded = true;
+ }
+ return _TimeSpan;
+ }
+ }
+ ///
+ /// The number of beats per minute followed at this moment.
+ ///
+ public float BPM
+ {
+ get
+ {
+ if (!_isBPMLoaded)
+ {
+ _BPM = _calculator?.BeatsPerMinuteOf(this) ?? throw new InvalidRDBeatException();
+ _isBPMLoaded = true;
+ }
+ return _BPM;
+ }
+ }
+ ///
+ /// The number of beats per bar followed at this moment.
+ ///
+ public float CPB
+ {
+ get
+ {
+ if (!_isCPBLoaded)
+ {
+ _CPB = (uint)Math.Round(_calculator?.CrotchetsPerBarOf(this) ?? throw new InvalidRDBeatException());
+ _isCPBLoaded = true;
+ }
+ return _CPB;
+ }
+ }
+ ///
+ /// Construct an instance without specifying a calculator.
+ ///
+ /// The total number of beats from this moment to the beginning of the level.
+ public RDBeat(float beatOnly)
+ {
+ this = default;
+ if (beatOnly < 1)
+ throw new OverflowException(string.Format("The beat must not be less than 1, but {0} is given", beatOnly));
+ _beat = beatOnly - 1f;
+ _isBeatLoaded = true;
+ }
+ ///
+ /// Constructs an instance of RDBeat with the specified bar and beat.
+ ///
+ /// The actual bar of this moment. Must be greater than or equal to 1.
+ /// The actual beat of this moment. Must be greater than or equal to 1.
+ /// Thrown when the bar or beat is less than 1.
+ public RDBeat(uint bar, float beat)
+ {
+ this = default;
+ if (bar < 1)
+ throw new OverflowException(string.Format("The bar must not be less than 1, but {0} is given", bar));
+ if (beat < 1)
+ throw new OverflowException(string.Format("The beat must not be less than 1, but {0} is given", beat));
+ _BarBeat = new ValueTuple(bar, beat);
+ _isBarBeatLoaded = true;
+ }
+ ///
+ /// Constructs an instance of RDBeat with the specified time span.
+ ///
+ /// The total amount of time from the start of the level to the moment.
+ /// Thrown when the time span is less than zero.
+ public RDBeat(TimeSpan timeSpan)
+ {
+ this = default;
+ if (timeSpan < TimeSpan.Zero)
+ throw new OverflowException(string.Format("The time must not be less than zero, but {0} is given", timeSpan));
+ _TimeSpan = timeSpan;
+ _isTimeSpanLoaded = true;
+ }
+ ///
+ /// Construct an instance with specifying a calculator.
+ ///
+ /// Specified calculator.
+ /// The total number of beats from this moment to the beginning of the level.
+ public RDBeat(BeatCalculator? calculator, float beatOnly)
+ {
+ this = new RDBeat(beatOnly);
+ _calculator = calculator;
+ }
+ ///
+ /// Construct an instance with specifying a calculator.
+ ///
+ /// Specified calculator.
+ /// The actual bar of this moment.
+ /// The actual beat of this moment.
+ public RDBeat(BeatCalculator calculator, uint bar, float beat)
+ {
+ this = new RDBeat(bar, beat);
+ _calculator = calculator;
+ _beat = _calculator.BarBeatToBeatOnly(bar, beat) - 1f;
+ }
+ ///
+ /// Construct an instance with specifying a calculator.
+ ///
+ /// Specified calculator.
+ /// The total amount of time from the start of the level to the moment
+ public RDBeat(BeatCalculator calculator, TimeSpan timeSpan)
+ {
+ this = new RDBeat(timeSpan);
+ _calculator = calculator;
+ _beat = _calculator.TimeSpanToBeatOnly(timeSpan) - 1f;
+ }
+ ///
+ /// Construct an instance with specifying a calculator.
+ ///
+ /// Specified calculator.
+ /// Another instance.
+ public RDBeat(BeatCalculator calculator, RDBeat beat)
+ {
+ this = default;
+ if (beat._isBeatLoaded)
+ {
+ if (beat._beat < 0f)
+ throw new OverflowException(string.Format("The beat must not be less than 1, but {0} is given", beat._beat));
+ _beat = beat._beat;
+ _isBeatLoaded = true;
+ _calculator = calculator;
+ }
+ else if (beat._isBarBeatLoaded)
+ {
+ if (beat._BarBeat.Bar < 1)
+ throw new OverflowException(string.Format("The bar must not be less than 1, but {0} is given", beat._BarBeat.Bar));
+ if (beat._BarBeat.Beat < 1)
+ throw new OverflowException(string.Format("The beat must not be less than 1, but {0} is given", beat._BarBeat.Beat));
+ _BarBeat = beat._BarBeat;
+ _isBarBeatLoaded = true;
+ _calculator = calculator;
+ _beat = _calculator.BarBeatToBeatOnly(beat._BarBeat.Bar, beat._BarBeat.Beat) - 1f;
+ }
+ else if (beat._isTimeSpanLoaded)
+ {
+ if (beat._TimeSpan < TimeSpan.Zero)
+ throw new OverflowException(string.Format("The time must not be less than zero, but {0} is given", beat._TimeSpan));
+ _TimeSpan = beat._TimeSpan;
+ _isTimeSpanLoaded = true;
+ _calculator = calculator;
+ _beat = _calculator.TimeSpanToBeatOnly(TimeSpan) - 1f;
+ }
+ }
+ ///
+ /// Construct a beat of the 1st beat from the calculator
+ ///
+ /// Specified calculator.
+ /// The first beat tied to the level.
+ public static RDBeat Default(BeatCalculator calculator)
+ {
+ RDBeat Default = new(calculator, 1f);
+ return Default;
+ }
+ ///
+ /// Determine if two beats come from the same level
+ ///
+ /// A beat.
+ /// Another beat.
+ /// If true, an exception will be thrown when two beats do not come from the same level.
+ ///
+ public static bool FromSameLevel(RDBeat a, RDBeat b, bool @throw = false) =>
+ (a._calculator?.Equals(b._calculator) ?? true)
+ || (@throw ? throw new RhythmBaseException("Beats must come from the same RDLevel.") : false);
+ ///
+ /// Determine if two beats are from the same level.
+ ///
+ /// If any of them does not come from any level, it will also return true.
+ ///
+ /// A beat.
+ /// Another beat.
+ /// If true, an exception will be thrown when two beats do not come from the same level.
+ ///
+ public static bool FromSameLevelOrNull(RDBeat a, RDBeat b, bool @throw = false) => a._calculator == null || b._calculator == null || FromSameLevel(a, b, @throw);
+ ///
+ /// Determine if two beats are from the same level.
+ ///
+ /// Another beat.
+ /// If true, an exception will be thrown when two beats do not come from the same level.
+ ///
+ [MemberNotNullWhen(true)]
+ public readonly bool FromSameLevel(RDBeat b, bool @throw = false) => FromSameLevel(this, b, @throw);
+ ///
+ /// Determine if two beats are from the same level.
+ ///
+ /// If any of them does not come from any level, it will also return true.
+ ///
+ /// Another beat.
+ /// If true, an exception will be thrown when two beats do not come from the same level.
+ ///
+ public readonly bool FromSameLevelOrNull(RDBeat b, bool @throw = false) => BaseLevel == null || b.BaseLevel == null || FromSameLevel(b, @throw);
+ ///
+ /// Returns a new instance of unbinding the level.
+ ///
+ /// A new instance of unbinding the level.
+ public readonly RDBeat WithoutBinding()
+ {
+ RDBeat result = this;
+ if (result._calculator != null)
+ result.Cache();
+ result._calculator = null;
+ return result;
+ }
+ ///
+ /// 校验这个实例是否缺少在转换单位时必需的参数并抛出异常
+ ///
+ ///
+ internal readonly void IfNullThrowException()
+ {
+ if (IsEmpty)
+ {
+ throw new InvalidRDBeatException();
+ }
+ }
+ ///
+ /// Refresh the cache.
+ ///
+ public void ResetCache()
+ {
+ object __ = BeatOnly;
+ _isBarBeatLoaded = false;
+ _isTimeSpanLoaded = false;
+ }
+ ///
+ /// Caches the current state of the beat by accessing its properties.
+ ///
+ /// Thrown when the beat cannot be calculated.
+ public void Cache()
+ {
+ IfNullThrowException();
+ object __ = BeatOnly;
+ __ = BarBeat;
+ __ = TimeSpan;
+ }
+ ///
+ ///
+ ///
+ internal void ResetBPM()
+ {
+ if (!_isBeatLoaded)
+ _beat = (_calculator?.TimeSpanToBeatOnly(_TimeSpan) - 1f) ?? throw new InvalidRDBeatException();
+ _isBeatLoaded = true;
+ _isTimeSpanLoaded = false;
+ _isBPMLoaded = false;
+ }
+ internal void ResetCPB()
+ {
+ if (!_isBeatLoaded)
+ _beat = (_calculator?.BarBeatToBeatOnly(_BarBeat.Bar, _BarBeat.Beat) - 1f) ?? throw new InvalidRDBeatException();
+ _isBeatLoaded = true;
+ _isBarBeatLoaded = false;
+ _isCPBLoaded = false;
+ }
+ ///
+ public static RDBeat operator +(RDBeat a, float b)
+ {
+ RDBeat result;
+ if (!a.IsEmpty)
+ result = new RDBeat(a._calculator, a.BeatOnly + b);
+ else
+ {
+ if (!a._isBeatLoaded)
+ throw new ArgumentNullException(nameof(a), "The beat cannot be calculate.");
+ result = new RDBeat(a._beat + b);
+ }
+ return result;
+ }
+ ///
+ public static RDBeat operator +(RDBeat a, TimeSpan b)
+ {
+ RDBeat result;
+ if (!a.IsEmpty)
+ result = new RDBeat(a._calculator, a.TimeSpan + b);
+ else
+ {
+ if (!a._isBeatLoaded)
+ throw new ArgumentNullException(nameof(a), "The beat cannot be calculate.");
+ result = new RDBeat(a._TimeSpan + b);
+ }
+ return result;
+ }
+ ///
+ public static RDBeat operator -(RDBeat a, float b)
+ {
+ RDBeat result;
+ if (!a.IsEmpty)
+ result = new RDBeat(a._calculator, a.BeatOnly - b);
+ else
+ {
+ if (!a._isBeatLoaded)
+ throw new ArgumentNullException(nameof(a), "The beat cannot be calculate.");
+ result = new RDBeat(a._beat - b);
+ }
+ return result;
+ }
+ ///
+ public static RDBeat operator -(RDBeat a, TimeSpan b)
+ {
+ RDBeat result;
+ if (!a.IsEmpty)
+ result = new RDBeat(a._calculator, a.TimeSpan - b);
+ else
+ {
+ if (!a._isBeatLoaded)
+ throw new ArgumentNullException(nameof(a), "The beat cannot be calculate.");
+ result = new RDBeat(a._TimeSpan - b);
+ }
+ return result;
+ }
+ ///
+ public static bool operator >(RDBeat a, RDBeat b) => FromSameLevel(a, b, true) && a.BeatOnly > b.BeatOnly;
+ ///
+ public static bool operator <(RDBeat a, RDBeat b) => FromSameLevel(a, b, true) && a.BeatOnly < b.BeatOnly;
+ ///
+ public static bool operator >=(RDBeat a, RDBeat b) => FromSameLevel(a, b, true) && a.BeatOnly >= b.BeatOnly;
+ ///
+ public static bool operator <=(RDBeat a, RDBeat b) => FromSameLevel(a, b, true) && a.BeatOnly <= b.BeatOnly;
+ ///
+ public static bool operator ==(RDBeat a, RDBeat b) => (FromSameLevel(a, b, true) && a._beat == b._beat) || (a._isBarBeatLoaded && b._isBarBeatLoaded && a._BarBeat.Bar == b._BarBeat.Bar && a._BarBeat.Beat == b._BarBeat.Beat) || (a._isTimeSpanLoaded && b._isTimeSpanLoaded && a._TimeSpan == b._TimeSpan) || a.BeatOnly == b.BeatOnly;
+ ///
+ public static bool operator !=(RDBeat a, RDBeat b) => !(a == b);
+ ///
+ public override string ToString()
+ {
+ string ToString;
+ if (IsEmpty)
+ ToString = string.Format("[{0},{1},{2}]", _isBeatLoaded ? _beat.ToString() : "?", _isBarBeatLoaded ? _BarBeat.ToString() : "?", _isTimeSpanLoaded ? _TimeSpan.ToString() : "?");
+ else
+ ToString = string.Format("[{0},{1}]", BarBeat.bar, BarBeat.beat);
+ return ToString;
+ }
+ ///
+ public readonly override bool Equals([NotNullWhen(true)] object? obj) => obj is RDBeat e && Equals(e);
+ ///
+ public readonly bool Equals(RDBeat other) => this == other;
+ ///
+ public override int GetHashCode() => HashCode.Combine(BeatOnly, BaseLevel);
+ ///
+ public int CompareTo(RDBeat other)
+ {
+ float result = BeatOnly - other.BeatOnly;
+ int CompareTo;
+ if (result > 0f)
+ CompareTo = 1;
+ else if (result < 0f)
+ CompareTo = -1;
+ else
+ CompareTo = 0;
+ return CompareTo;
+ }
+ internal BeatCalculator? _calculator;
+ private bool _isBeatLoaded;
+ private bool _isBarBeatLoaded;
+ private bool _isTimeSpanLoaded;
+ private bool _isBPMLoaded;
+ private bool _isCPBLoaded;
+ private float _beat;
+ private (uint Bar, float Beat) _BarBeat;
+ private TimeSpan _TimeSpan;
+ private float _BPM;
+ private uint _CPB;
+ }
+}
diff --git a/RhythmBaseCore/Components/RDCharacter.cs b/RhythmBaseCore/Components/RDCharacter.cs
new file mode 100644
index 0000000..2d5e23e
--- /dev/null
+++ b/RhythmBaseCore/Components/RDCharacter.cs
@@ -0,0 +1,52 @@
+
+namespace RhythmBase.Components;
+
+///
+/// A Character.
+///
+public readonly struct RDCharacter
+{
+ ///
+ /// Whether in-game character or customized character(sprite).
+ ///
+ public bool IsCustom { get; }
+ ///
+ /// In-game character.
+ ///
+ /// If using a customized character, this value will be empty
+ ///
+ public RDCharacters? Character { get; }
+ ///
+ /// Customized character(sprite).
+ ///
+ /// If using an in-game character, this value will be empty
+ ///
+ public string? CustomCharacter { get; }
+ ///
+ /// Construct an in-game character.
+ ///
+ /// Character type.
+ public RDCharacter(RDCharacters character)
+ {
+ IsCustom = false;
+ Character = character;
+ }
+ ///
+ /// Construct a customized character.
+ ///
+ /// A sprite.
+ public RDCharacter(string character)
+ {
+ IsCustom = true;
+ CustomCharacter = character;
+ }
+ ///
+ public static implicit operator RDCharacter(RDCharacters character) => new(character);
+ ///
+ public static implicit operator RDCharacter(string character) => new(character);
+ ///
+ public override readonly string ToString() => (IsCustom
+ ? (CustomCharacter)
+ : Character?.ToString())
+ ?? "[Null]";
+}
diff --git a/RhythmBaseCore/Components/RDCharacters.cs b/RhythmBaseCore/Components/RDCharacters.cs
new file mode 100644
index 0000000..5cee747
--- /dev/null
+++ b/RhythmBaseCore/Components/RDCharacters.cs
@@ -0,0 +1,96 @@
+namespace RhythmBase.Components
+{
+ ///
+ /// In-game character.
+ ///
+ public enum RDCharacters
+ {
+#pragma warning disable CS1591
+ Adog,
+ Allison,
+ Athlete,
+ AthletePhysio,
+ Barista,
+ Beat,
+#if DEBUG
+ BlankCPU,
+#endif
+ Bodybuilder,
+ Boy,
+ BoyRaya,
+ BoyTangzhuang,
+ Buro,
+#if DEBUG
+ Canary,
+#endif
+ Clef,
+ Cockatiel,
+ ColeGuitar,
+ ColeSynth,
+ Controller,
+#if DEBUG
+ Custom,
+#endif
+ DancingCouple,
+ Edega,
+ Farmer,
+ FarmerAlternate,
+ Girl,
+ GirlCNY,
+ HoodieBoy,
+ HoodieBoyAlternate,
+ HoodieBoyBlue,
+ Ian,
+ IanBubble,
+ Janitor,
+ Kanye,
+ Lucia,
+ LuckyBag,
+ LuckyBaseball,
+ LuckyIce,
+#if DEBUG
+ LuckyJersey,
+#endif
+ Lune,
+ Marija,
+ Miner,
+ MrsStevendog,
+ MrsStevenson,
+ MrStevendog,
+ MrStevenson,
+ NicoleCigs,
+ NicoleCoffee,
+ NicoleMints,
+ None,
+ Oriole,
+#if DEBUG
+ Otto,
+#endif
+ Owl,
+ Paige,
+ Parrot,
+ Player,
+ Politician,
+ Purritician,
+ Quaver,
+ Rin,
+ Rodney,
+ Saturday,
+ Samurai,
+ SamuraiBaseball,
+ SamuraiBlue,
+ SamuraiBoss,
+ SamuraiBossAlt,
+ SamuraiGirl,
+ SamuraiGreen,
+ SamuraiPirate,
+ SamuraiYellow,
+ SmokinBarista,
+ Tentacle,
+ Treble,
+ Weeknd,
+#if DEBUG
+ Wren
+#endif
+ }
+}
diff --git a/RhythmBaseCore/Components/RDColor.cs b/RhythmBaseCore/Components/RDColor.cs
new file mode 100644
index 0000000..f1181c2
--- /dev/null
+++ b/RhythmBaseCore/Components/RDColor.cs
@@ -0,0 +1,1420 @@
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using System.Numerics;
+
+namespace RhythmBase.Components
+{
+ ///
+ /// Represents a color with red, green, blue, and alpha components.
+ ///
+ [DebuggerDisplay($"{{{nameof(GetDebuggerDisplay)}(),nq}}")]
+ public struct RDColor(uint hex) :
+ IEqualityOperators,
+ IEquatable, IFormattable
+ {
+ ///
+ /// #AARRGGBB
+ ///
+ private uint color = hex;
+ ///
+ /// Gets or sets the alpha component of the color.
+ ///
+ public byte A
+ {
+ readonly get => (byte)((color >> 24) & 0xFFu);
+ set => color = (color & 0x00FFFFFFu) | ((uint)value << 24);
+ }
+ ///
+ /// Gets or sets the red component of the color.
+ ///
+ public byte R
+ {
+ readonly get => (byte)((color >> 16) & 0xFFu);
+ set => color = (color & 0xFF00FFFFu) | ((uint)value << 16);
+ }
+ ///
+ /// Gets or sets the green component of the color.
+ ///
+ public byte G
+ {
+ readonly get => (byte)((color >> 8) & 0xFFu);
+ set => color = (color & 0xFFFF00FFu) | ((uint)value << 8);
+ }
+ ///
+ /// Gets or sets the blue component of the color.
+ ///
+ public byte B
+ {
+ readonly get => (byte)(color & 0xFFu);
+ set => color = (color & 0xFFFFFF00u) | value;
+ }
+ ///
+ /// Returns a new RDColor instance with the specified alpha value.
+ ///
+ /// The alpha value to set.
+ /// A new RDColor instance with the specified alpha value.
+ public readonly RDColor WithAlpha(byte alpha) => new((color & 0x00FFFFFFu) | ((uint)alpha << 24));
+ ///
+ /// Returns a new RDColor instance with the specified red value.
+ ///
+ /// The red value to set.
+ /// A new RDColor instance with the specified red value.
+ public readonly RDColor WithRed(byte red) => new((color & 0xFF00FFFFu) | ((uint)red << 16));
+ ///
+ /// Returns a new RDColor instance with the specified green value.
+ ///
+ /// The green value to set.
+ /// A new RDColor instance with the specified green value.
+ public readonly RDColor WithGreen(byte green) => new((color & 0xFFFF00FFu) | ((uint)green << 8));
+ ///
+ /// Returns a new RDColor instance with the specified blue value.
+ ///
+ /// The blue value to set.
+ /// A new RDColor instance with the specified blue value.
+ public readonly RDColor WithBlue(byte blue) => new((color & 0xFFFFFF00u) | blue);
+ ///
+ /// Converts the color to HSL (Hue, Saturation, Lightness) color space.
+ ///
+ /// The hue component.
+ /// The saturation component.
+ /// The lightness component.
+ public readonly void ToHsl(out float h, out float s, out float l)
+ {
+ float r = this.R / 255f;
+ float g = this.G / 255f;
+ float b = this.B / 255f;
+
+ float max = Math.Max(r, Math.Max(g, b));
+ float min = Math.Min(r, Math.Min(g, b));
+ float delta = max - min;
+
+ h = 0;
+ if (delta != 0)
+ {
+ if (max == r)
+ {
+ h = (g - b) / delta + (g < b ? 6 : 0);
+ }
+ else if (max == g)
+ {
+ h = (b - r) / delta + 2;
+ }
+ else
+ {
+ h = (r - g) / delta + 4;
+ }
+ h /= 6;
+ }
+
+ l = (max + min) / 2;
+ s = delta == 0 ? 0 : delta / (1 - Math.Abs(2 * l - 1));
+
+ h *= 360;
+ s *= 100;
+ l *= 100;
+ }
+ ///
+ /// Converts the color to HSV (Hue, Saturation, Value) color space.
+ ///
+ /// The hue component.
+ /// The saturation component.
+ /// The value component.
+ public readonly void ToHsv(out float h, out float s, out float v)
+ {
+ float r = this.R / 255f;
+ float g = this.G / 255f;
+ float b = this.B / 255f;
+
+ float max = Math.Max(r, Math.Max(g, b));
+ float min = Math.Min(r, Math.Min(g, b));
+ float delta = max - min;
+
+ h = 0f;
+ if (delta != 0)
+ {
+ if (max == r)
+ {
+ h = (g - b) / delta + (g < b ? 6 : 0);
+ }
+ else if (max == g)
+ {
+ h = (b - r) / delta + 2;
+ }
+ else if (max == b)
+ {
+ h = (r - g) / delta + 4;
+ }
+ h /= 6;
+ }
+
+ s = max == 0 ? 0 : delta / max;
+ v = max;
+
+ h *= 360;
+ s *= 100;
+ v *= 100;
+ }
+
+ ///
+ /// Creates an RDColor instance from RGBA values.
+ ///
+ /// Red component
+ /// Green component
+ /// Blue component
+ /// Alpha component (default is 255)
+ /// RDColor instance
+ public static RDColor FromRgba(byte r, byte g, byte b, byte a = 255) => new((uint)(a << 24 | r << 16 | g << 8 | b));
+ ///
+ /// Creates an RDColor instance from a hexadecimal string.
+ /// Supports hexadecimal strings of length 3, 4, 6, or 8.
+ ///
+ /// Hexadecimal string
+ /// RDColor instance
+ /// Thrown when the hexadecimal string length is not 3, 4, 6, or 8
+ public static RDColor FromRgba(string hex)
+ {
+ if (TryFromRgba(hex, out RDColor color))
+ return color;
+ throw new ArgumentException("Hex string must be 3, 4, 6, or 8 characters long.");
+ }
+ ///
+ /// Tries to create an RDColor instance from a hexadecimal string.
+ /// Supports hexadecimal strings of length 3, 4, 6, or 8.
+ ///
+ /// The hexadecimal string representing the color.
+ /// When this method returns, contains the RDColor instance created from the hexadecimal string, if the conversion succeeded, or the default value if the conversion failed.
+ /// true if the hexadecimal string was converted successfully; otherwise, false.
+ public static bool TryFromRgba(string hex, [MaybeNullWhen(false)] out RDColor color)
+ {
+ hex = hex.Trim();
+ if (hex.StartsWith('#'))
+ hex = hex[1..];
+ string? hex2 = hex.Length switch
+ {
+ 3 => $"FF{hex[0]}{hex[0]}{hex[1]}{hex[1]}{hex[2]}{hex[2]}",
+ 4 => $"{hex[3]}{hex[3]}{hex[0]}{hex[0]}{hex[1]}{hex[1]}{hex[2]}{hex[2]}",
+ 6 => $"FF{hex}",
+ 8 => $"{hex[6..8]}{hex[0..6]}",
+ _ => null,
+ };
+ if (hex2 is null)
+ {
+ color = default;
+ return false;
+ }
+ color = new RDColor(Convert.ToUInt32(hex2, 16));
+ return true;
+ }
+ ///
+ /// Creates an RDColor instance from a 32-bit RGBA value.
+ ///
+ /// The 32-bit RGBA value.
+ /// A new RDColor instance.
+ public static RDColor FromRgba(uint hex)
+ {
+ uint argb = (hex & 0x00FFFFFFu) | ((hex & 0xFFu) << 24);
+ return new RDColor(argb);
+ }
+ ///
+ /// Creates an RDColor instance from ARGB values.
+ ///
+ /// Alpha component
+ /// Red component
+ /// Green component
+ /// Blue component
+ /// RDColor instance
+ public static RDColor FromArgb(byte a, byte r, byte g, byte b) => new((uint)(a << 24 | r << 16 | g << 8 | b));
+ ///
+ /// Creates an RDColor instance from a hexadecimal string in ARGB format.
+ /// Supports hexadecimal strings of length 3, 4, 6, or 8.
+ ///
+ /// Hexadecimal string
+ /// RDColor instance
+ /// Thrown when the hexadecimal string length is not 3, 4, 6, or 8
+ public static RDColor FromArgb(string hex)
+ {
+ hex = hex.Trim();
+ if (hex.StartsWith('#'))
+ hex = hex[1..];
+
+ hex = hex.Length switch
+ {
+ 3 => $"FF{hex[0]}{hex[0]}{hex[1]}{hex[1]}{hex[2]}{hex[2]}",
+ 4 => $"{hex[0]}{hex[0]}{hex[1]}{hex[1]}{hex[2]}{hex[2]}{hex[3]}{hex[3]}",
+ 6 => $"FF{hex}",
+ 8 => hex,
+ _ => throw new ArgumentException("Hex string must be 3, 4, 6, or 8 characters long."),
+ };
+
+ return new RDColor(Convert.ToUInt32(hex, 16));
+ }
+ ///
+ /// Tries to create an RDColor instance from a hexadecimal string.
+ /// Supports hexadecimal strings of length 3, 4, 6, or 8.
+ ///
+ /// The hexadecimal string representing the color.
+ /// When this method returns, contains the RDColor instance created from the hexadecimal string, if the conversion succeeded, or the default value if the conversion failed.
+ /// true if the hexadecimal string was converted successfully; otherwise, false.
+ public static bool TryFromArgb(string hex, [MaybeNullWhen(false)] out RDColor color)
+ {
+ hex = hex.Trim();
+ if (hex.StartsWith('#'))
+ hex = hex[1..];
+ string? hex2 = hex.Length switch
+ {
+ 3 => $"FF{hex[0]}{hex[0]}{hex[1]}{hex[1]}{hex[2]}{hex[2]}",
+ 4 => $"{hex[0]}{hex[0]}{hex[1]}{hex[1]}{hex[2]}{hex[2]}{hex[3]}{hex[3]}",
+ 6 => $"FF{hex}",
+ 8 => hex,
+ _ => null,
+ };
+ if (hex2 is null)
+ {
+ color = default;
+ return false;
+ }
+ color = new RDColor(Convert.ToUInt32(hex2, 16));
+ return true;
+ }
+ ///
+ /// Creates an instance from a 32-bit ARGB value.
+ ///
+ /// The 32-bit ARGB value.
+ /// A new instance.
+ public static RDColor FromArgb(uint hex) => new(hex);
+ ///
+ /// Creates an RDColor object from the HSL color space.
+ ///
+ /// Hue (0-360)
+ /// Saturation (0-1)
+ /// Lightness (0-1)
+ /// Alpha (0-255)
+ /// Corresponding RDColor object
+ public static RDColor FromHsl(float h, float s, float l, byte a = 255)
+ {
+ h /= 360;
+ s /= 100;
+ l /= 100;
+ float r, g, b;
+ if (s == 0)
+ {
+ r = g = b = l; // achromatic
+ }
+ else
+ {
+ static float hue2rgb(float p, float q, float t)
+ {
+ if (t < 0) t += 1;
+ if (t > 1) t -= 1;
+ if (t < 1 / 6.0) return p + (q - p) * 6 * t;
+ if (t < 1 / 2.0) return q;
+ if (t < 2 / 3.0) return p + (q - p) * (2 / 3.0f - t) * 6;
+ return p;
+ }
+
+ float q = l < 0.5 ? l * (1 + s) : l + s - l * s;
+ float p = 2 * l - q;
+ r = hue2rgb(p, q, h + 1 / 3.0f);
+ g = hue2rgb(p, q, h);
+ b = hue2rgb(p, q, h - 1 / 3.0f);
+ }
+
+ return new RDColor((uint)(a << 24 | (int)(r * 255) << 16 | (int)(g * 255) << 8 | (int)(b * 255)));
+ }
+ ///
+ /// Creates an RDColor object from HSV values.
+ ///
+ /// Hue (0-360)
+ /// Saturation (0-1)
+ /// Value (0-1)
+ /// Alpha (0-255)
+ /// Returns an RDColor object.
+ public static RDColor FromHsv(float h, float s, float v, byte a = 255)
+ {
+ h /= 360;
+ s /= 100;
+ v /= 100;
+
+ int hi = (int)(h * 6);
+ float f = h * 6 - hi;
+ float p = v * (1 - s);
+ float q = v * (1 - f * s);
+ float t = v * (1 - (1 - f) * s);
+
+ float r = 0, g = 0, b = 0;
+ switch (hi % 6)
+ {
+ case 0: r = v; g = t; b = p; break;
+ case 1: r = q; g = v; b = p; break;
+ case 2: r = p; g = v; b = t; break;
+ case 3: r = p; g = q; b = v; break;
+ case 4: r = t; g = p; b = v; break;
+ case 5: r = v; g = p; b = q; break;
+ }
+
+ return new RDColor((uint)(a << 24 | (int)(r * 255) << 16 | (int)(g * 255) << 8 | (int)(b * 255)));
+ }
+ ///
+ /// Creates an RDColor instance from a color name.
+ ///
+ /// The name of the color.
+ /// An RDColor instance corresponding to the specified color name.
+ /// Thrown when the color name is invalid.
+ public static RDColor FromName(string name)
+ {
+ if (TryFromName(name, out RDColor color))
+ return color;
+ throw new ArgumentException($"Invalid color name: {name}.");
+ }
+ ///
+ /// Tries to create an RDColor instance from a color name.
+ ///
+ /// The name of the color.
+ /// When this method returns, contains the RDColor instance created from the color name, if the conversion succeeded, or the default value if the conversion failed.
+ /// true if the color name was converted successfully; otherwise, false.
+ public static bool TryFromName(string name, [MaybeNullWhen(false)] out RDColor color)
+ {
+ RDColor? color2 = name.ToLower() switch
+ {
+ "aliceblue" => AliceBlue,
+ "antiquewhite" => AntiqueWhite,
+ "aqua" => Aqua,
+ "aquamarine" => Aquamarine,
+ "azure" => Azure,
+ "beige" => Beige,
+ "bisque" => Bisque,
+ "black" => Black,
+ "blanchedalmond" => BlanchedAlmond,
+ "blue" => Blue,
+ "blueviolet" => BlueViolet,
+ "brown" => Brown,
+ "burlywood" => BurlyWood,
+ "cadetblue" => CadetBlue,
+ "chartreuse" => Chartreuse,
+ "chocolate" => Chocolate,
+ "coral" => Coral,
+ "cornflowerblue" => CornflowerBlue,
+ "cornsilk" => Cornsilk,
+ "crimson" => Crimson,
+ "cyan" => Cyan,
+ "darkblue" => DarkBlue,
+ "darkcyan" => DarkCyan,
+ "darkgoldenrod" => DarkGoldenrod,
+ "darkgray" => DarkGray,
+ "darkgreen" => DarkGreen,
+ "darkkhaki" => DarkKhaki,
+ "darkmagenta" => DarkMagenta,
+ "darkolivegreen" => DarkOliveGreen,
+ "darkorange" => DarkOrange,
+ "darkorchid" => DarkOrchid,
+ "darkred" => DarkRed,
+ "darksalmon" => DarkSalmon,
+ "darkseagreen" => DarkSeaGreen,
+ "darkslateblue" => DarkSlateBlue,
+ "darkslategray" => DarkSlateGray,
+ "darkturquoise" => DarkTurquoise,
+ "darkviolet" => DarkViolet,
+ "deeppink" => DeepPink,
+ "deepskyblue" => DeepSkyBlue,
+ "dimgray" => DimGray,
+ "dodgerblue" => DodgerBlue,
+ "firebrick" => Firebrick,
+ "floralwhite" => FloralWhite,
+ "forestgreen" => ForestGreen,
+ "fuchsia" => Fuchsia,
+ "gainsboro" => Gainsboro,
+ "ghostwhite" => GhostWhite,
+ "gold" => Gold,
+ "goldenrod" => Goldenrod,
+ "gray" => Gray,
+ "green" => Green,
+ "greenyellow" => GreenYellow,
+ "honeydew" => Honeydew,
+ "hotpink" => HotPink,
+ "indianred" => IndianRed,
+ "indigo" => Indigo,
+ "ivory" => Ivory,
+ "khaki" => Khaki,
+ "lavender" => Lavender,
+ "lavenderblush" => LavenderBlush,
+ "lawngreen" => LawnGreen,
+ "lemonchiffon" => LemonChiffon,
+ "lightblue" => LightBlue,
+ "lightcoral" => LightCoral,
+ "lightcyan" => LightCyan,
+ "lightgoldenrodyellow" => LightGoldenrodYellow,
+ "lightgray" => LightGray,
+ "lightgreen" => LightGreen,
+ "lightpink" => LightPink,
+ "lightsalmon" => LightSalmon,
+ "lightseagreen" => LightSeaGreen,
+ "lightskyblue" => LightSkyBlue,
+ "lightslategray" => LightSlateGray,
+ "lightsteelblue" => LightSteelBlue,
+ "lightyellow" => LightYellow,
+ "lime" => Lime,
+ "limegreen" => LimeGreen,
+ "linen" => Linen,
+ "magenta" => Magenta,
+ "maroon" => Maroon,
+ "mediumaquamarine" => MediumAquamarine,
+ "mediumblue" => MediumBlue,
+ "mediumorchid" => MediumOrchid,
+ "mediumpurple" => MediumPurple,
+ "mediumseagreen" => MediumSeaGreen,
+ "mediumslateblue" => MediumSlateBlue,
+ "mediumspringgreen" => MediumSpringGreen,
+ "mediumturquoise" => MediumTurquoise,
+ "mediumvioletred" => MediumVioletRed,
+ "midnightblue" => MidnightBlue,
+ "mintcream" => MintCream,
+ "mistyrose" => MistyRose,
+ "moccasin" => Moccasin,
+ "navajowhite" => NavajoWhite,
+ "navy" => Navy,
+ "oldlace" => OldLace,
+ "olive" => Olive,
+ "olivedrab" => OliveDrab,
+ "orange" => Orange,
+ "orangered" => OrangeRed,
+ "orchid" => Orchid,
+ "palegoldenrod" => PaleGoldenrod,
+ "palegreen" => PaleGreen,
+ "paleturquoise" => PaleTurquoise,
+ "palevioletred" => PaleVioletRed,
+ "papayawhip" => PapayaWhip,
+ "peachpuff" => PeachPuff,
+ "peru" => Peru,
+ "pink" => Pink,
+ "plum" => Plum,
+ "powderblue" => PowderBlue,
+ "purple" => Purple,
+ "red" => Red,
+ "rosybrown" => RosyBrown,
+ "royalblue" => RoyalBlue,
+ "saddlebrown" => SaddleBrown,
+ "salmon" => Salmon,
+ "sandybrown" => SandyBrown,
+ "seagreen" => SeaGreen,
+ "seashell" => SeaShell,
+ "sienna" => Sienna,
+ "silver" => Silver,
+ "skyblue" => SkyBlue,
+ "slateblue" => SlateBlue,
+ "slategray" => SlateGray,
+ "snow" => Snow,
+ "springgreen" => SpringGreen,
+ "steelblue" => SteelBlue,
+ "tan" => Tan,
+ "teal" => Teal,
+ "thistle" => Thistle,
+ "tomato" => Tomato,
+ "turquoise" => Turquoise,
+ "violet" => Violet,
+ "wheat" => Wheat,
+ "white" => White,
+ "whitesmoke" => WhiteSmoke,
+ "yellow" => Yellow,
+ "yellowgreen" => YellowGreen,
+ "transparent" => Transparent,
+ "empty" => Empty,
+ _ => null,
+ };
+ if (color2 is null)
+ {
+ color = default;
+ return false;
+ }
+ color = color2.Value;
+ return true;
+ }
+ ///
+ /// Tries to get the name(s) of the color.
+ ///
+ /// When this method returns, contains the name(s) of the color if the conversion succeeded, or an empty array if the conversion failed.
+ /// true if the color name(s) were found; otherwise, false.
+ ///
+ /// For colors with multiple names, the CMYK name is preferred.
+ ///
+ public readonly bool TryGetName([NotNullWhen(true)] out string[] names)
+ {
+ names = color switch
+ {
+ 4293982463u => ["AliceBlue"],
+ 4294634455u => ["AntiqueWhite"],
+ 4278255615u => ["Cyan", "Aqua"],
+ 4286578644u => ["Aquamarine"],
+ 4293984255u => ["Azure"],
+ 4294309340u => ["Beige"],
+ 4294960324u => ["Bisque"],
+ 4278190080u => ["Black"],
+ 4294962125u => ["BlanchedAlmond"],
+ 4278190335u => ["Blue"],
+ 4287245282u => ["BlueViolet"],
+ 4289014314u => ["Brown"],
+ 4292786311u => ["BurlyWood"],
+ 4284456608u => ["CadetBlue"],
+ 4286578432u => ["Chartreuse"],
+ 4291979550u => ["Chocolate"],
+ 4294934352u => ["Coral"],
+ 4284782061u => ["CornflowerBlue"],
+ 4294965468u => ["Cornsilk"],
+ 4292613180u => ["Crimson"],
+ 4278190219u => ["DarkBlue"],
+ 4278225803u => ["DarkCyan"],
+ 4290283019u => ["DarkGoldenrod"],
+ 4289309097u => ["DarkGray"],
+ 4278215680u => ["DarkGreen"],
+ 4290623339u => ["DarkKhaki"],
+ 4287299723u => ["DarkMagenta"],
+ 4283788079u => ["DarkOliveGreen"],
+ 4294937600u => ["DarkOrange"],
+ 4288230092u => ["DarkOrchid"],
+ 4287299584u => ["DarkRed"],
+ 4293498490u => ["DarkSalmon"],
+ 4287609995u => ["DarkSeaGreen"],
+ 4282924427u => ["DarkSlateBlue"],
+ 4281290575u => ["DarkSlateGray"],
+ 4278243025u => ["DarkTurquoise"],
+ 4287889619u => ["DarkViolet"],
+ 4294907027u => ["DeepPink"],
+ 4278239231u => ["DeepSkyBlue"],
+ 4285098345u => ["DimGray"],
+ 4280193279u => ["DodgerBlue"],
+ 4289864226u => ["Firebrick"],
+ 4294966000u => ["FloralWhite"],
+ 4280453922u => ["ForestGreen"],
+ 4294902015u => ["Magenta", "Fuchsia"],
+ 4292664540u => ["Gainsboro"],
+ 4294506751u => ["GhostWhite"],
+ 4294956800u => ["Gold"],
+ 4292519200u => ["Goldenrod"],
+ 4286611584u => ["Gray"],
+ 4278222848u => ["Green"],
+ 4289593135u => ["GreenYellow"],
+ 4293984240u => ["Honeydew"],
+ 4294928820u => ["HotPink"],
+ 4291648604u => ["IndianRed"],
+ 4283105410u => ["Indigo"],
+ 4294967280u => ["Ivory"],
+ 4293977740u => ["Khaki"],
+ 4293322490u => ["Lavender"],
+ 4294963445u => ["LavenderBlush"],
+ 4286381056u => ["LawnGreen"],
+ 4294965965u => ["LemonChiffon"],
+ 4289583334u => ["LightBlue"],
+ 4293951616u => ["LightCoral"],
+ 4292935679u => ["LightCyan"],
+ 4294638290u => ["LightGoldenrodYellow"],
+ 4292072403u => ["LightGray"],
+ 4287688336u => ["LightGreen"],
+ 4294948545u => ["LightPink"],
+ 4294942842u => ["LightSalmon"],
+ 4280332970u => ["LightSeaGreen"],
+ 4287090426u => ["LightSkyBlue"],
+ 4286023833u => ["LightSlateGray"],
+ 4289774814u => ["LightSteelBlue"],
+ 4294967264u => ["LightYellow"],
+ 4278255360u => ["Lime"],
+ 4281519410u => ["LimeGreen"],
+ 4294635750u => ["Linen"],
+ 4286578688u => ["Maroon"],
+ 4284927402u => ["MediumAquamarine"],
+ 4278190285u => ["MediumBlue"],
+ 4290401747u => ["MediumOrchid"],
+ 4287852763u => ["MediumPurple"],
+ 4282168177u => ["MediumSeaGreen"],
+ 4286277870u => ["MediumSlateBlue"],
+ 4278254234u => ["MediumSpringGreen"],
+ 4282962380u => ["MediumTurquoise"],
+ 4291237253u => ["MediumVioletRed"],
+ 4279834992u => ["MidnightBlue"],
+ 4294311930u => ["MintCream"],
+ 4294960353u => ["MistyRose"],
+ 4294960309u => ["Moccasin"],
+ 4294958765u => ["NavajoWhite"],
+ 4278190208u => ["Navy"],
+ 4294833638u => ["OldLace"],
+ 4286611456u => ["Olive"],
+ 4285238819u => ["OliveDrab"],
+ 4294944000u => ["Orange"],
+ 4294919424u => ["OrangeRed"],
+ 4292505814u => ["Orchid"],
+ 4293847210u => ["PaleGoldenrod"],
+ 4288215960u => ["PaleGreen"],
+ 4289720046u => ["PaleTurquoise"],
+ 4292571283u => ["PaleVioletRed"],
+ 4294963157u => ["PapayaWhip"],
+ 4294957753u => ["PeachPuff"],
+ 4291659071u => ["Peru"],
+ 4294951115u => ["Pink"],
+ 4292714717u => ["Plum"],
+ 4289781990u => ["PowderBlue"],
+ 4286578816u => ["Purple"],
+ 4294901760u => ["Red"],
+ 4290547599u => ["RosyBrown"],
+ 4282477025u => ["RoyalBlue"],
+ 4287317267u => ["SaddleBrown"],
+ 4294606962u => ["Salmon"],
+ 4294222944u => ["SandyBrown"],
+ 4281240407u => ["SeaGreen"],
+ 4294964718u => ["SeaShell"],
+ 4288696877u => ["Sienna"],
+ 4290822336u => ["Silver"],
+ 4287090411u => ["SkyBlue"],
+ 4285160141u => ["SlateBlue"],
+ 4285563024u => ["SlateGray"],
+ 4294966010u => ["Snow"],
+ 4278255487u => ["SpringGreen"],
+ 4282811060u => ["SteelBlue"],
+ 4291998860u => ["Tan"],
+ 4278222976u => ["Teal"],
+ 4292394968u => ["Thistle"],
+ 4294927175u => ["Tomato"],
+ 4282441936u => ["Turquoise"],
+ 4293821166u => ["Violet"],
+ 4294303411u => ["Wheat"],
+ uint.MaxValue => ["White"],
+ 4294309365u => ["WhiteSmoke"],
+ 4294967040u => ["Yellow"],
+ 4288335154u => ["YellowGreen"],
+ 16777215u => ["Transparent"],
+ 0u => ["Empty"],
+ _ => [],
+ };
+ if (names.Length == 0)
+ return false;
+ return true;
+ }
+ ///
+ public static bool operator ==(RDColor left, RDColor right) => left.color == right.color;
+ ///
+ public static bool operator !=(RDColor left, RDColor right) => left.color != right.color;
+ ///
+ public override readonly string ToString() => ToString("#AARRGGBB");
+ private readonly string GetDebuggerDisplay() => ToString();
+ ///
+ public readonly bool Equals(RDColor other) => color == other.color;
+ ///
+ public override readonly bool Equals([NotNullWhen(true)] object? obj) => obj is RDColor e && Equals(e);
+ ///
+ public override readonly int GetHashCode() => color.GetHashCode();
+ ///
+ public readonly string ToString(string? format, IFormatProvider? formatProvider) =>
+ (formatProvider ?? new RDColorFormatInfo()).GetFormat(typeof(ICustomFormatter)) is ICustomFormatter formatter
+ ? formatter.Format(format, this, formatProvider)
+ : ToString();
+ ///
+ public readonly string ToString(string? format) => ToString(format, null);
+
+ ///
+ ///Gets the predefined color of alice blue, or #FFF0F8FF.
+ ///
+ public static readonly RDColor AliceBlue = new(4293982463u);
+
+ ///
+ ///Gets the predefined color of antique white, or #FFFAEBD7.
+ ///
+ public static readonly RDColor AntiqueWhite = new(4294634455u);
+
+ ///
+ ///Gets the predefined color of aqua, or #FF00FFFF.
+ ///
+ public static readonly RDColor Aqua = new(4278255615u);
+
+ ///
+ ///Gets the predefined color of aquamarine, or #FF7FFFD4.
+ ///
+ public static readonly RDColor Aquamarine = new(4286578644u);
+
+ ///
+ ///Gets the predefined color of azure, or #FFF0FFFF.
+ ///
+ public static readonly RDColor Azure = new(4293984255u);
+
+ ///
+ ///Gets the predefined color of beige, or #FFF5F5DC.
+ ///
+ public static readonly RDColor Beige = new(4294309340u);
+
+ ///
+ ///Gets the predefined color of bisque, or #FFFFE4C4.
+ ///
+ public static readonly RDColor Bisque = new(4294960324u);
+
+ ///
+ ///Gets the predefined color of black, or #FF000000.
+ ///
+ public static readonly RDColor Black = new(4278190080u);
+
+ ///
+ ///Gets the predefined color of blanched almond, or #FFFFEBCD.
+ ///
+ public static readonly RDColor BlanchedAlmond = new(4294962125u);
+
+ ///
+ ///Gets the predefined color of blue, or #FF0000FF.
+ ///
+ public static readonly RDColor Blue = new(4278190335u);
+
+ ///
+ ///Gets the predefined color of blue violet, or #FF8A2BE2.
+ ///
+ public static readonly RDColor BlueViolet = new(4287245282u);
+
+ ///
+ ///Gets the predefined color of brown, or #FFA52A2A.
+ ///
+ public static readonly RDColor Brown = new(4289014314u);
+
+ ///
+ ///Gets the predefined color of burly wood, or #FFDEB887.
+ ///
+ public static readonly RDColor BurlyWood = new(4292786311u);
+
+ ///
+ ///Gets the predefined color of cadet blue, or #FF5F9EA0.
+ ///
+ public static readonly RDColor CadetBlue = new(4284456608u);
+
+ ///
+ ///Gets the predefined color of chartreuse, or #FF7FFF00.
+ ///
+ public static readonly RDColor Chartreuse = new(4286578432u);
+
+ ///
+ ///Gets the predefined color of chocolate, or #FFD2691E.
+ ///
+ public static readonly RDColor Chocolate = new(4291979550u);
+
+ ///
+ ///Gets the predefined color of coral, or #FFFF7F50.
+ ///
+ public static readonly RDColor Coral = new(4294934352u);
+
+ ///
+ ///Gets the predefined color of cornflower blue, or #FF6495ED.
+ ///
+ public static readonly RDColor CornflowerBlue = new(4284782061u);
+
+ ///
+ ///Gets the predefined color of cornsilk, or #FFFFF8DC.
+ ///
+ public static readonly RDColor Cornsilk = new(4294965468u);
+
+ ///
+ ///Gets the predefined color of crimson, or #FFDC143C.
+ ///
+ public static readonly RDColor Crimson = new(4292613180u);
+
+ ///
+ ///Gets the predefined color of cyan, or #FF00FFFF.
+ ///
+ public static readonly RDColor Cyan = new(4278255615u);
+
+ ///
+ ///Gets the predefined color of dark blue, or #FF00008B.
+ ///
+ public static readonly RDColor DarkBlue = new(4278190219u);
+
+ ///
+ ///Gets the predefined color of dark cyan, or #FF008B8B.
+ ///
+ public static readonly RDColor DarkCyan = new(4278225803u);
+
+ ///
+ ///Gets the predefined color of dark goldenrod, or #FFB8860B.
+ ///
+ public static readonly RDColor DarkGoldenrod = new(4290283019u);
+
+ ///
+ ///Gets the predefined color of dark gray, or #FFA9A9A9.
+ ///
+ public static readonly RDColor DarkGray = new(4289309097u);
+
+ ///
+ ///Gets the predefined color of dark green, or #FF006400.
+ ///
+ public static readonly RDColor DarkGreen = new(4278215680u);
+
+ ///
+ ///Gets the predefined color of dark khaki, or #FFBDB76B.
+ ///
+ public static readonly RDColor DarkKhaki = new(4290623339u);
+
+ ///
+ ///Gets the predefined color of dark magenta, or #FF8B008B.
+ ///
+ public static readonly RDColor DarkMagenta = new(4287299723u);
+
+ ///
+ ///Gets the predefined color of dark olive green, or #FF556B2F.
+ ///
+ public static readonly RDColor DarkOliveGreen = new(4283788079u);
+
+ ///
+ ///Gets the predefined color of dark orange, or #FFFF8C00.
+ ///
+ public static readonly RDColor DarkOrange = new(4294937600u);
+
+ ///
+ ///Gets the predefined color of dark orchid, or #FF9932CC.
+ ///
+ public static readonly RDColor DarkOrchid = new(4288230092u);
+
+ ///
+ ///Gets the predefined color of dark red, or #FF8B0000.
+ ///
+ public static readonly RDColor DarkRed = new(4287299584u);
+
+ ///
+ ///Gets the predefined color of dark salmon, or #FFE9967A.
+ ///
+ public static readonly RDColor DarkSalmon = new(4293498490u);
+
+ ///
+ ///Gets the predefined color of dark sea green, or #FF8FBC8B.
+ ///
+ public static readonly RDColor DarkSeaGreen = new(4287609995u);
+
+ ///
+ ///Gets the predefined color of dark slate blue, or #FF483D8B.
+ ///
+ public static readonly RDColor DarkSlateBlue = new(4282924427u);
+
+ ///
+ ///Gets the predefined color of dark slate gray, or #FF2F4F4F.
+ ///
+ public static readonly RDColor DarkSlateGray = new(4281290575u);
+
+ ///
+ ///Gets the predefined color of dark turquoise, or #FF00CED1.
+ ///
+ public static readonly RDColor DarkTurquoise = new(4278243025u);
+
+ ///
+ ///Gets the predefined color of dark violet, or #FF9400D3.
+ ///
+ public static readonly RDColor DarkViolet = new(4287889619u);
+
+ ///
+ ///Gets the predefined color of deep pink, or #FFFF1493.
+ ///
+ public static readonly RDColor DeepPink = new(4294907027u);
+
+ ///
+ ///Gets the predefined color of deep sky blue, or #FF00BFFF.
+ ///
+ public static readonly RDColor DeepSkyBlue = new(4278239231u);
+
+ ///
+ ///Gets the predefined color of dim gray, or #FF696969.
+ ///
+ public static readonly RDColor DimGray = new(4285098345u);
+
+ ///
+ ///Gets the predefined color of dodger blue, or #FF1E90FF.
+ ///
+ public static readonly RDColor DodgerBlue = new(4280193279u);
+
+ ///
+ ///Gets the predefined color of firebrick, or #FFB22222.
+ ///
+ public static readonly RDColor Firebrick = new(4289864226u);
+
+ ///
+ ///Gets the predefined color of floral white, or #FFFFFAF0.
+ ///
+ public static readonly RDColor FloralWhite = new(4294966000u);
+
+ ///
+ ///Gets the predefined color of forest green, or #FF228B22.
+ ///
+ public static readonly RDColor ForestGreen = new(4280453922u);
+
+ ///
+ ///Gets the predefined color of fuchsia, or #FFFF00FF.
+ ///
+ public static readonly RDColor Fuchsia = new(4294902015u);
+
+ ///
+ ///Gets the predefined color of gainsboro, or #FFDCDCDC.
+ ///
+ public static readonly RDColor Gainsboro = new(4292664540u);
+
+ ///
+ ///Gets the predefined color of ghost white, or #FFF8F8FF.
+ ///
+ public static readonly RDColor GhostWhite = new(4294506751u);
+
+ ///
+ ///Gets the predefined color of gold, or #FFFFD700.
+ ///
+ public static readonly RDColor Gold = new(4294956800u);
+
+ ///
+ ///Gets the predefined color of goldenrod, or #FFDAA520.
+ ///
+ public static readonly RDColor Goldenrod = new(4292519200u);
+
+ ///
+ ///Gets the predefined color of gray, or #FF808080.
+ ///
+ public static readonly RDColor Gray = new(4286611584u);
+
+ ///
+ ///Gets the predefined color of green, or #FF008000.
+ ///
+ public static readonly RDColor Green = new(4278222848u);
+
+ ///
+ ///Gets the predefined color of green yellow, or #FFADFF2F.
+ ///
+ public static readonly RDColor GreenYellow = new(4289593135u);
+
+ ///
+ ///Gets the predefined color of honeydew, or #FFF0FFF0.
+ ///
+ public static readonly RDColor Honeydew = new(4293984240u);
+
+ ///
+ ///Gets the predefined color of hot pink, or #FFFF69B4.
+ ///
+ public static readonly RDColor HotPink = new(4294928820u);
+
+ ///
+ ///Gets the predefined color of indian red, or #FFCD5C5C.
+ ///
+ public static readonly RDColor IndianRed = new(4291648604u);
+
+ ///
+ ///Gets the predefined color of indigo, or #FF4B0082.
+ ///
+ public static readonly RDColor Indigo = new(4283105410u);
+
+ ///
+ ///Gets the predefined color of ivory, or #FFFFFFF0.
+ ///
+ public static readonly RDColor Ivory = new(4294967280u);
+
+ ///
+ ///Gets the predefined color of khaki, or #FFF0E68C.
+ ///
+ public static readonly RDColor Khaki = new(4293977740u);
+
+ ///
+ ///Gets the predefined color of lavender, or #FFE6E6FA.
+ ///
+ public static readonly RDColor Lavender = new(4293322490u);
+
+ ///
+ ///Gets the predefined color of lavender blush, or #FFFFF0F5.
+ ///
+ public static readonly RDColor LavenderBlush = new(4294963445u);
+
+ ///
+ ///Gets the predefined color of lawn green, or #FF7CFC00.
+ ///
+ public static readonly RDColor LawnGreen = new(4286381056u);
+
+ ///
+ ///Gets the predefined color of lemon chiffon, or #FFFFFACD.
+ ///
+ public static readonly RDColor LemonChiffon = new(4294965965u);
+
+ ///
+ ///Gets the predefined color of light blue, or #FFADD8E6.
+ ///
+ public static readonly RDColor LightBlue = new(4289583334u);
+
+ ///
+ ///Gets the predefined color of light coral, or #FFF08080.
+ ///
+ public static readonly RDColor LightCoral = new(4293951616u);
+
+ ///
+ ///Gets the predefined color of light cyan, or #FFE0FFFF.
+ ///
+ public static readonly RDColor LightCyan = new(4292935679u);
+
+ ///
+ ///Gets the predefined color of light goldenrod yellow, or #FFFAFAD2.
+ ///
+ public static readonly RDColor LightGoldenrodYellow = new(4294638290u);
+
+ ///
+ ///Gets the predefined color of light gray, or #FFD3D3D3.
+ ///
+ public static readonly RDColor LightGray = new(4292072403u);
+
+ ///
+ ///Gets the predefined color of light green, or #FF90EE90.
+ ///
+ public static readonly RDColor LightGreen = new(4287688336u);
+
+ ///
+ ///Gets the predefined color of light pink, or #FFFFB6C1.
+ ///
+ public static readonly RDColor LightPink = new(4294948545u);
+
+ ///
+ ///Gets the predefined color of light salmon, or #FFFFA07A.
+ ///
+ public static readonly RDColor LightSalmon = new(4294942842u);
+
+ ///
+ ///Gets the predefined color of light sea green, or #FF20B2AA.
+ ///
+ public static readonly RDColor LightSeaGreen = new(4280332970u);
+
+ ///
+ ///Gets the predefined color of light sky blue, or #FF87CEFA.
+ ///
+ public static readonly RDColor LightSkyBlue = new(4287090426u);
+
+ ///
+ ///Gets the predefined color of light slate gray, or #FF778899.
+ ///
+ public static readonly RDColor LightSlateGray = new(4286023833u);
+
+ ///
+ ///Gets the predefined color of light steel blue, or #FFB0C4DE.
+ ///
+ public static readonly RDColor LightSteelBlue = new(4289774814u);
+
+ ///
+ ///Gets the predefined color of light yellow, or #FFFFFFE0.
+ ///
+ public static readonly RDColor LightYellow = new(4294967264u);
+
+ ///
+ ///Gets the predefined color of lime, or #FF00FF00.
+ ///
+ public static readonly RDColor Lime = new(4278255360u);
+
+ ///
+ ///Gets the predefined color of lime green, or #FF32CD32.
+ ///
+ public static readonly RDColor LimeGreen = new(4281519410u);
+
+ ///
+ ///Gets the predefined color of linen, or #FFFAF0E6.
+ ///
+ public static readonly RDColor Linen = new(4294635750u);
+
+ ///
+ ///Gets the predefined color of magenta, or #FFFF00FF.
+ ///
+ public static readonly RDColor Magenta = new(4294902015u);
+
+ ///
+ ///Gets the predefined color of maroon, or #FF800000.
+ ///
+ public static readonly RDColor Maroon = new(4286578688u);
+
+ ///
+ ///Gets the predefined color of medium aquamarine, or #FF66CDAA.
+ ///
+ public static readonly RDColor MediumAquamarine = new(4284927402u);
+
+ ///
+ ///Gets the predefined color of medium blue, or #FF0000CD.
+ ///
+ public static readonly RDColor MediumBlue = new(4278190285u);
+
+ ///
+ ///Gets the predefined color of medium orchid, or #FFBA55D3.
+ ///
+ public static readonly RDColor MediumOrchid = new(4290401747u);
+
+ ///
+ ///Gets the predefined color of medium purple, or #FF9370DB.
+ ///
+ public static readonly RDColor MediumPurple = new(4287852763u);
+
+ ///
+ ///Gets the predefined color of medium sea green, or #FF3CB371.
+ ///
+ public static readonly RDColor MediumSeaGreen = new(4282168177u);
+
+ ///
+ ///Gets the predefined color of medium slate blue, or #FF7B68EE.
+ ///
+ public static readonly RDColor MediumSlateBlue = new(4286277870u);
+
+ ///
+ ///Gets the predefined color of medium spring green, or #FF00FA9A.
+ ///
+ public static readonly RDColor MediumSpringGreen = new(4278254234u);
+
+ ///
+ ///Gets the predefined color of medium turquoise, or #FF48D1CC.
+ ///
+ public static readonly RDColor MediumTurquoise = new(4282962380u);
+
+ ///
+ ///Gets the predefined color of medium violet red, or #FFC71585.
+ ///
+ public static readonly RDColor MediumVioletRed = new(4291237253u);
+
+ ///
+ ///Gets the predefined color of midnight blue, or #FF191970.
+ ///
+ public static readonly RDColor MidnightBlue = new(4279834992u);
+
+ ///
+ ///Gets the predefined color of mint cream, or #FFF5FFFA.
+ ///
+ public static readonly RDColor MintCream = new(4294311930u);
+
+ ///
+ ///Gets the predefined color of misty rose, or #FFFFE4E1.
+ ///
+ public static readonly RDColor MistyRose = new(4294960353u);
+
+ ///
+ ///Gets the predefined color of moccasin, or #FFFFE4B5.
+ ///
+ public static readonly RDColor Moccasin = new(4294960309u);
+
+ ///
+ ///Gets the predefined color of navajo white, or #FFFFDEAD.
+ ///
+ public static readonly RDColor NavajoWhite = new(4294958765u);
+
+ ///
+ ///Gets the predefined color of navy, or #FF000080.
+ ///
+ public static readonly RDColor Navy = new(4278190208u);
+
+ ///
+ ///Gets the predefined color of old lace, or #FFFDF5E6.
+ ///
+ public static readonly RDColor OldLace = new(4294833638u);
+
+ ///
+ ///Gets the predefined color of olive, or #FF808000.
+ ///
+ public static readonly RDColor Olive = new(4286611456u);
+
+ ///
+ ///Gets the predefined color of olive drab, or #FF6B8E23.
+ ///
+ public static readonly RDColor OliveDrab = new(4285238819u);
+
+ ///
+ ///Gets the predefined color of orange, or #FFFFA500.
+ ///
+ public static readonly RDColor Orange = new(4294944000u);
+
+ ///
+ ///Gets the predefined color of orange red, or #FFFF4500.
+ ///
+ public static readonly RDColor OrangeRed = new(4294919424u);
+
+ ///
+ ///Gets the predefined color of orchid, or #FFDA70D6.
+ ///
+ public static readonly RDColor Orchid = new(4292505814u);
+
+ ///
+ ///Gets the predefined color of pale goldenrod, or #FFEEE8AA.
+ ///
+ public static readonly RDColor PaleGoldenrod = new(4293847210u);
+
+ ///
+ ///Gets the predefined color of pale green, or #FF98FB98.
+ ///
+ public static readonly RDColor PaleGreen = new(4288215960u);
+
+ ///
+ ///Gets the predefined color of pale turquoise, or #FFAFEEEE.
+ ///
+ public static readonly RDColor PaleTurquoise = new(4289720046u);
+
+ ///
+ ///Gets the predefined color of pale violet red, or #FFDB7093.
+ ///
+ public static readonly RDColor PaleVioletRed = new(4292571283u);
+
+ ///
+ ///Gets the predefined color of papaya whip, or #FFFFEFD5.
+ ///
+ public static readonly RDColor PapayaWhip = new(4294963157u);
+
+ ///
+ ///Gets the predefined color of peach puff, or #FFFFDAB9.
+ ///
+ public static readonly RDColor PeachPuff = new(4294957753u);
+
+ ///
+ ///Gets the predefined color of peru, or #FFCD853F.
+ ///
+ public static readonly RDColor Peru = new(4291659071u);
+
+ ///
+ ///Gets the predefined color of pink, or #FFFFC0CB.
+ ///
+ public static readonly RDColor Pink = new(4294951115u);
+
+ ///
+ ///Gets the predefined color of plum, or #FFDDA0DD.
+ ///
+ public static readonly RDColor Plum = new(4292714717u);
+
+ ///
+ ///Gets the predefined color of powder blue, or #FFB0E0E6.
+ ///
+ public static readonly RDColor PowderBlue = new(4289781990u);
+
+ ///
+ ///Gets the predefined color of purple, or #FF800080.
+ ///
+ public static readonly RDColor Purple = new(4286578816u);
+
+ ///
+ ///Gets the predefined color of red, or #FFFF0000.
+ ///
+ public static readonly RDColor Red = new(4294901760u);
+
+ ///
+ ///Gets the predefined color of rosy brown, or #FFBC8F8F.
+ ///
+ public static readonly RDColor RosyBrown = new(4290547599u);
+
+ ///
+ ///Gets the predefined color of royal blue, or #FF4169E1.
+ ///
+ public static readonly RDColor RoyalBlue = new(4282477025u);
+
+ ///
+ ///Gets the predefined color of saddle brown, or #FF8B4513.
+ ///
+ public static readonly RDColor SaddleBrown = new(4287317267u);
+
+ ///
+ ///Gets the predefined color of salmon, or #FFFA8072.
+ ///
+ public static readonly RDColor Salmon = new(4294606962u);
+
+ ///
+ ///Gets the predefined color of sandy brown, or #FFF4A460.
+ ///
+ public static readonly RDColor SandyBrown = new(4294222944u);
+
+ ///
+ ///Gets the predefined color of sea green, or #FF2E8B57.
+ ///
+ public static readonly RDColor SeaGreen = new(4281240407u);
+
+ ///
+ ///Gets the predefined color of sea shell, or #FFFFF5EE.
+ ///
+ public static readonly RDColor SeaShell = new(4294964718u);
+
+ ///
+ ///Gets the predefined color of sienna, or #FFA0522D.
+ ///
+ public static readonly RDColor Sienna = new(4288696877u);
+
+ ///
+ ///Gets the predefined color of silver, or #FFC0C0C0.
+ ///
+ public static readonly RDColor Silver = new(4290822336u);
+
+ ///
+ ///Gets the predefined color of sky blue, or #FF87CEEB.
+ ///
+ public static readonly RDColor SkyBlue = new(4287090411u);
+
+ ///
+ ///Gets the predefined color of slate blue, or #FF6A5ACD.
+ ///
+ public static readonly RDColor SlateBlue = new(4285160141u);
+
+ ///
+ ///Gets the predefined color of slate gray, or #FF708090.
+ ///
+ public static readonly RDColor SlateGray = new(4285563024u);
+
+ ///
+ ///Gets the predefined color of snow, or #FFFFFAFA.
+ ///
+ public static readonly RDColor Snow = new(4294966010u);
+
+ ///
+ ///Gets the predefined color of spring green, or #FF00FF7F.
+ ///
+ public static readonly RDColor SpringGreen = new(4278255487u);
+
+ ///
+ ///Gets the predefined color of steel blue, or #FF4682B4.
+ ///
+ public static readonly RDColor SteelBlue = new(4282811060u);
+
+ ///
+ ///Gets the predefined color of tan, or #FFD2B48C.
+ ///
+ public static readonly RDColor Tan = new(4291998860u);
+
+ ///
+ ///Gets the predefined color of teal, or #FF008080.
+ ///
+ public static readonly RDColor Teal = new(4278222976u);
+
+ ///
+ ///Gets the predefined color of thistle, or #FFD8BFD8.
+ ///
+ public static readonly RDColor Thistle = new(4292394968u);
+
+ ///
+ ///Gets the predefined color of tomato, or #FFFF6347.
+ ///
+ public static readonly RDColor Tomato = new(4294927175u);
+
+ ///
+ ///Gets the predefined color of turquoise, or #FF40E0D0.
+ ///
+ public static readonly RDColor Turquoise = new(4282441936u);
+
+ ///
+ ///Gets the predefined color of violet, or #FFEE82EE.
+ ///
+ public static readonly RDColor Violet = new(4293821166u);
+
+ ///
+ ///Gets the predefined color of wheat, or #FFF5DEB3.
+ ///
+ public static readonly RDColor Wheat = new(4294303411u);
+
+ ///
+ ///Gets the predefined color of white, or #FFFFFFFF.
+ ///
+ public static readonly RDColor White = new(uint.MaxValue);
+
+ ///
+ ///Gets the predefined color of white smoke, or #FFF5F5F5.
+ ///
+ public static readonly RDColor WhiteSmoke = new(4294309365u);
+
+ ///
+ ///Gets the predefined color of yellow, or #FFFFFF00.
+ ///
+ public static readonly RDColor Yellow = new(4294967040u);
+
+ ///
+ ///Gets the predefined color of yellow green, or #FF9ACD32.
+ ///
+ public static readonly RDColor YellowGreen = new(4288335154u);
+
+ ///
+ ///Gets the predefined color of white transparent, or #00FFFFFF.
+ ///
+ public static readonly RDColor Transparent = new(16777215u);
+
+ ///
+ ///Gets the predefined empty color (black transparent), or #00000000.
+ ///
+ public static readonly RDColor Empty = new(0u);
+ }
+}
\ No newline at end of file
diff --git a/RhythmBaseCore/Components/RDColorFormatInfo.cs b/RhythmBaseCore/Components/RDColorFormatInfo.cs
new file mode 100644
index 0000000..1f5889a
--- /dev/null
+++ b/RhythmBaseCore/Components/RDColorFormatInfo.cs
@@ -0,0 +1,67 @@
+using System.Text.RegularExpressions;
+
+namespace RhythmBase.Components
+{
+ ///
+ /// Provides a custom format provider for RDColor.
+ ///
+ internal class RDColorFormatInfo : IFormatProvider
+ {
+ ///
+ public object? GetFormat(Type? formatType) => formatType == typeof(ICustomFormatter) ? new RDColorFormatter() : (object?)null;
+ }
+
+ ///
+ /// Custom formatter for RDColor.
+ ///
+ internal class RDColorFormatter : ICustomFormatter, IFormatProvider
+ {
+ ///
+ public string Format(string? format, object? arg, IFormatProvider? formatProvider)
+ {
+ return arg is RDColor color
+ ? format switch
+ {
+ "RRGGBB" => $"{color.R:X2}{color.G:X2}{color.B:X2}",
+ "RRGGBBAA" => $"{color.R:X2}{color.G:X2}{color.B:X2}{color.A:X2}",
+ "AARRGGBB" => $"{color.A:X2}{color.R:X2}{color.G:X2}{color.B:X2}",
+ "#RRGGBB" => $"#{color.R:X2}{color.G:X2}{color.B:X2}",
+ "#RRGGBBAA" => $"#{color.R:X2}{color.G:X2}{color.B:X2}{color.A:X2}",
+ "#AARRGGBB" => $"#{color.A:X2}{color.R:X2}{color.G:X2}{color.B:X2}",
+ "rrggbb" => $"{color.R:x2}{color.G:x2}{color.B:x2}",
+ "rrggbbaa" => $"{color.R:x2}{color.G:x2}{color.B:x2}{color.A:x2}",
+ "aarrggbb" => $"{color.A:x2}{color.R:x2}{color.G:x2}{color.B:x2}",
+ "#rrggbb" => $"#{color.R:x2}{color.G:x2}{color.B:x2}",
+ "#rrggbbaa" => $"#{color.R:x2}{color.G:x2}{color.B:x2}{color.A:x2}",
+ "#aarrggbb" => $"#{color.A:x2}{color.R:x2}{color.G:x2}{color.B:x2}",
+ "R,G,B" or "r,g,b" => $"{color.R},{color.G},{color.B}",
+ "R,G,B,A" or "r,g,b,a" => $"{color.R},{color.G},{color.B},{color.A}",
+ "A,R,G,B" or "a,r,g,b" => $"{color.A},{color.R},{color.G},{color.B}",
+ "RGB" or "rgb" => $"R:{color.R},G:{color.G},B:{color.B}",
+ "RGBA" or "rgba" => $"R:{color.R},G:{color.G},B:{color.B},A:{color.A}",
+ "ARGB" or "argb" or _ => $"A:{color.A},R:{color.R},G:{color.G},B:{color.B}",
+ }
+ : arg?.ToString() ?? string.Empty;
+ }
+
+ private static string ReplaceColorComponent(string format, char component, int value)
+ {
+ int startIndex = 0;
+ while ((startIndex = format.IndexOf(component, startIndex)) != -1)
+ {
+ int length = 1;
+ while (startIndex + length < format.Length && format[startIndex + length] == component)
+ {
+ length++;
+ }
+ string replacement = char.IsUpper(component) ? value.ToString($"X{length}") : value.ToString();
+ format = string.Concat(format.AsSpan(0, startIndex), replacement, format.AsSpan(startIndex + length));
+ startIndex += replacement.Length;
+ }
+ return format;
+ }
+
+ ///
+ public object? GetFormat(Type? formatType) => formatType == typeof(ICustomFormatter) ? this : (object?)null;
+ }
+}
diff --git a/RhythmBaseCore/Components/RDExpression.cs b/RhythmBaseCore/Components/RDExpression.cs
new file mode 100644
index 0000000..96a8997
--- /dev/null
+++ b/RhythmBaseCore/Components/RDExpression.cs
@@ -0,0 +1,456 @@
+using Newtonsoft.Json;
+using RhythmBase.Converters;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.Numerics;
+namespace RhythmBase.Components
+{
+ ///
+ /// An Expression
+ ///
+ [JsonConverter(typeof(ExpressionConverter))]
+ public struct RDExpression : INumber
+ {
+ ///
+ /// Gets the numeric value of the expression.
+ ///
+ public float NumericValue { get; }
+ ///
+ /// Gets the expression value as a string.
+ ///
+ public readonly string ExpressionValue
+ {
+ get
+ {
+ bool isNumeric = IsNumeric;
+ string ExpressionValue = isNumeric ? NumericValue.ToString() : _exp;
+ return ExpressionValue;
+ }
+ }
+ ///
+ /// Gets the evaluated value of the expression.
+ ///
+ public readonly float Value => IsNumeric ? NumericValue : Calculate(ExpressionValue);
+ private static float Calculate(string exp)
+ {
+ if (string.IsNullOrWhiteSpace(exp))
+ return 0;
+ if (float.TryParse(exp, out float result))
+ return result;
+ return 0;
+ }
+ static RDExpression INumberBase.One => 1;
+ static int INumberBase.Radix => 10;
+ ///
+ /// Gets the additive identity for the type.
+ ///
+ public static RDExpression Zero => 0;
+ static RDExpression IAdditiveIdentity.AdditiveIdentity => 0;
+ static RDExpression IMultiplicativeIdentity.MultiplicativeIdentity => 1;
+ ///
+ /// Initializes a new instance of the struct with a numeric value.
+ ///
+ /// The numeric value of the expression.
+ public RDExpression(float value)
+ {
+ this = default;
+ IsNumeric = true;
+ NumericValue = value;
+ }
+ ///
+ /// Initializes a new instance of the struct with a string value.
+ ///
+ /// The string value of the expression.
+ public RDExpression([AllowNull] string value)
+ {
+ IsNumeric = float.TryParse(value, out float numeric);
+ if (IsNumeric)
+ NumericValue = numeric;
+ else
+ _exp = value ?? "";
+ }
+ ///
+ public override readonly bool Equals([NotNullWhen(true)] object? obj) => obj is RDExpression e && Equals(e);
+ ///
+ public readonly bool Equals(RDExpression other) => (IsNumeric == other.IsNumeric && NumericValue == other.NumericValue) || _exp == other._exp;
+ ///
+ public override readonly int GetHashCode()
+ {
+ HashCode hash = default;
+ hash.Add(ExpressionValue);
+ return hash.ToHashCode();
+ }
+ ///
+ public override readonly string ToString() => ExpressionValue;
+ ///
+ /// Converts a string to a nullable RDExpression.
+ ///
+ /// The string to convert.
+ /// A nullable RDExpression if the string is not null or empty; otherwise, null.
+ public static RDExpression? Nullable(string s) => s != null && s.Length != 0 ? new RDExpression?(new RDExpression(s)) : null;
+
+ readonly int IComparable.CompareTo(object? obj)
+ {
+ if (obj is RDExpression other)
+ {
+ return CompareTo(other);
+ }
+ throw new ArgumentException("Object is not a RDExpression");
+ }
+ ///
+ public readonly int CompareTo(RDExpression other)
+ {
+ if (IsNumeric && other.IsNumeric)
+ {
+ return NumericValue.CompareTo(other.NumericValue);
+ }
+ return string.Compare(ExpressionValue, other.ExpressionValue, StringComparison.Ordinal);
+ }
+
+ static RDExpression INumberBase.Abs(RDExpression value)
+ {
+ return value.IsNumeric ? new RDExpression(Math.Abs(value.NumericValue)) : value;
+ }
+
+ static bool INumberBase.IsCanonical(RDExpression value)
+ {
+ return true;
+ }
+
+ static bool INumberBase.IsComplexNumber(RDExpression value)
+ {
+ return false;
+ }
+
+ static bool INumberBase.IsEvenInteger(RDExpression value)
+ {
+ return value.IsNumeric && value.NumericValue % 2 == 0;
+ }
+
+ static bool INumberBase.IsFinite(RDExpression value)
+ {
+ return value.IsNumeric && !float.IsInfinity(value.NumericValue);
+ }
+
+ static bool INumberBase.IsImaginaryNumber(RDExpression value)
+ {
+ return false;
+ }
+
+ static bool INumberBase.IsInfinity(RDExpression value)
+ {
+ return value.IsNumeric && float.IsInfinity(value.NumericValue);
+ }
+
+ static bool INumberBase.IsInteger(RDExpression value)
+ {
+ return value.IsNumeric && value.NumericValue == Math.Floor(value.NumericValue);
+ }
+
+ static bool INumberBase.IsNaN(RDExpression value)
+ {
+ return value.IsNumeric && float.IsNaN(value.NumericValue);
+ }
+
+ static bool INumberBase.IsNegative(RDExpression value)
+ {
+ return value.IsNumeric ? value.NumericValue < 0 : Calculate(value.ExpressionValue) < 0;
+ }
+
+ static bool INumberBase.IsNegativeInfinity(RDExpression value)
+ {
+ return value.IsNumeric && float.IsNegativeInfinity(value.NumericValue);
+ }
+
+ static bool INumberBase.IsNormal(RDExpression value)
+ {
+ return value.IsNumeric && !float.IsSubnormal(value.NumericValue);
+ }
+
+ static bool INumberBase.IsOddInteger(RDExpression value)
+ {
+ return value.IsNumeric && value.NumericValue % 2 != 0;
+ }
+
+ static bool INumberBase.IsPositive(RDExpression value)
+ {
+ return value.IsNumeric ? value.NumericValue > 0 : Calculate(value.ExpressionValue) > 0;
+ }
+
+ static bool INumberBase.IsPositiveInfinity(RDExpression value)
+ {
+ return value.IsNumeric && float.IsPositiveInfinity(value.NumericValue);
+ }
+
+ static bool INumberBase.IsRealNumber(RDExpression value)
+ {
+ return value.IsNumeric;
+ }
+
+ static bool INumberBase.IsSubnormal(RDExpression value)
+ {
+ return value.IsNumeric && float.IsSubnormal(value.NumericValue);
+ }
+
+ static bool INumberBase.IsZero(RDExpression value)
+ {
+ return value.IsNumeric ? value.NumericValue == 0 : Calculate(value.ExpressionValue) == 0;
+ }
+ static RDExpression INumberBase.MaxMagnitude(RDExpression x, RDExpression y)
+ {
+ return x.IsNumeric && y.IsNumeric ? (Math.Abs(x.NumericValue) > Math.Abs(y.NumericValue) ? x : y) : throw new NotImplementedException();
+ }
+
+ static RDExpression INumberBase.MaxMagnitudeNumber(RDExpression x, RDExpression y)
+ {
+ return x.IsNumeric && y.IsNumeric ? (Math.Abs(x.NumericValue) > Math.Abs(y.NumericValue) ? x : y) : throw new NotImplementedException();
+ }
+
+ static RDExpression INumberBase.MinMagnitude(RDExpression x, RDExpression y)
+ {
+ return x.IsNumeric && y.IsNumeric ? (Math.Abs(x.NumericValue) < Math.Abs(y.NumericValue) ? x : y) : throw new NotImplementedException();
+ }
+
+ static RDExpression INumberBase.MinMagnitudeNumber(RDExpression x, RDExpression y)
+ {
+ return x.IsNumeric && y.IsNumeric ? (Math.Abs(x.NumericValue) < Math.Abs(y.NumericValue) ? x : y) : throw new NotImplementedException();
+ }
+
+ static RDExpression INumberBase.Parse(ReadOnlySpan s, NumberStyles style, IFormatProvider? provider)
+ {
+ if (float.TryParse(s, style, provider, out float result))
+ {
+ return new RDExpression(result);
+ }
+ throw new FormatException("Input string was not in a correct format.");
+ }
+ static RDExpression INumberBase.Parse(string s, NumberStyles style, IFormatProvider? provider)
+ {
+ if (float.TryParse(s, style, provider, out float result))
+ {
+ return new RDExpression(result);
+ }
+ throw new FormatException("Input string was not in a correct format.");
+ }
+ static bool INumberBase.TryConvertFromChecked(TOther value, out RDExpression result)
+ {
+ result = new RDExpression();
+ return false;
+ }
+
+ static bool INumberBase.TryConvertFromSaturating(TOther value, out RDExpression result)
+ {
+ result = new RDExpression();
+ return false;
+ }
+
+ static bool INumberBase.TryConvertFromTruncating(TOther value, out RDExpression result)
+ {
+ result = new RDExpression();
+ return false;
+ }
+
+ static bool INumberBase.TryConvertToChecked(RDExpression value, out TOther result)
+ {
+ result = default!;
+ return false;
+ }
+
+ static bool INumberBase.TryConvertToSaturating(RDExpression value, out TOther result)
+ {
+ result = default!;
+ return false;
+ }
+
+ static bool INumberBase.TryConvertToTruncating(RDExpression value, out TOther result)
+ {
+ result = default!;
+ return false;
+ }
+
+ static bool INumberBase.TryParse(ReadOnlySpan s, NumberStyles style, IFormatProvider? provider, out RDExpression result)
+ {
+ result = new(s.ToString());
+ return true;
+ }
+
+ static bool INumberBase.TryParse(string? s, NumberStyles style, IFormatProvider? provider, out RDExpression result)
+ {
+ result = new(s ?? "0");
+ return true;
+ }
+
+ readonly bool ISpanFormattable.TryFormat(Span destination, out int charsWritten, ReadOnlySpan format, IFormatProvider? provider)
+ {
+ return NumericValue.TryFormat(destination, out charsWritten, format, provider);
+ }
+
+ readonly string IFormattable.ToString(string? format, IFormatProvider? formatProvider)
+ {
+ return NumericValue.ToString(format, formatProvider);
+ }
+
+ static RDExpression ISpanParsable.Parse(ReadOnlySpan s, IFormatProvider? provider)
+ {
+ if (float.TryParse(s, NumberStyles.Float | NumberStyles.AllowThousands, provider, out float result))
+ {
+ return new RDExpression(result);
+ }
+ throw new FormatException("Input string was not in a correct format.");
+ }
+
+ static bool ISpanParsable.TryParse(ReadOnlySpan s, IFormatProvider? provider, out RDExpression result)
+ {
+ if (float.TryParse(s, NumberStyles.Float | NumberStyles.AllowThousands, provider, out float numericResult))
+ {
+ result = new RDExpression(numericResult);
+ return true;
+ }
+ result = default;
+ return false;
+ }
+
+ static RDExpression IParsable.Parse(string s, IFormatProvider? provider)
+ {
+ if (float.TryParse(s, NumberStyles.Float | NumberStyles.AllowThousands, provider, out float result))
+ {
+ return new RDExpression(result);
+ }
+ throw new FormatException("Input string was not in a correct format.");
+ }
+
+ static bool IParsable.TryParse(string? s, IFormatProvider? provider, out RDExpression result)
+ {
+ if (float.TryParse(s, NumberStyles.Float | NumberStyles.AllowThousands, provider, out float numericResult))
+ {
+ result = new RDExpression(numericResult);
+ return true;
+ }
+ result = default;
+ return false;
+ }
+ ///
+ public static RDExpression operator +(RDExpression left, float right) => left.IsNumeric
+ ? new RDExpression(left.NumericValue + right)
+ : new RDExpression(string.Format("{0}+{1}", left.ExpressionValue, right));
+ ///
+ public static RDExpression operator +(float left, RDExpression right) => right.IsNumeric
+ ? new RDExpression(left + right.NumericValue)
+ : new RDExpression(string.Format("{0}+{1}", left, right.ExpressionValue));
+ ///
+ public static RDExpression operator +(RDExpression left, RDExpression right) => left.IsNumeric && right.IsNumeric
+ ? new RDExpression(left.NumericValue + right.NumericValue)
+ : new RDExpression(string.Format("{0}+{1}", left.ExpressionValue, right.ExpressionValue));
+ ///
+ public static RDExpression operator -(RDExpression left, float right) => left.IsNumeric
+ ? new RDExpression(left.NumericValue - right)
+ : new RDExpression(string.Format("{0}-{1}", left.ExpressionValue, right));
+ ///
+ public static RDExpression operator -(float left, RDExpression right) => right.IsNumeric
+ ? new RDExpression(left - right.NumericValue)
+ : new RDExpression(string.Format("{0}-{1}", left, right.ExpressionValue));
+ ///
+ public static RDExpression operator -(RDExpression left, RDExpression right) => left.IsNumeric && right.IsNumeric
+ ? new RDExpression(left.NumericValue - right.NumericValue)
+ : new RDExpression(string.Format("{0}-{1}", left.ExpressionValue, right.ExpressionValue));
+ ///
+ public static RDExpression operator *(RDExpression left, float right) => left.IsNumeric
+ ? new RDExpression(left.NumericValue * right)
+ : new RDExpression(string.Format("({0})*{1}", left.ExpressionValue, right));
+ ///
+ public static RDExpression operator *(float left, RDExpression right) => right.IsNumeric
+ ? new RDExpression(left * right.NumericValue)
+ : new RDExpression(string.Format("{0}*({1})", left, right.ExpressionValue));
+ ///
+ public static RDExpression operator *(RDExpression left, RDExpression right) => left.IsNumeric && right.IsNumeric
+ ? new RDExpression(left.NumericValue * right.NumericValue)
+ : new RDExpression(string.Format("({0})*({1})", left.ExpressionValue, right.ExpressionValue));
+ ///
+ public static RDExpression operator /(RDExpression left, float right) => left.IsNumeric
+ ? new RDExpression(left.NumericValue / right)
+ : new RDExpression(string.Format("({0})/{1}", left.ExpressionValue, right));
+ ///
+ public static RDExpression operator /(float left, RDExpression right) => right.IsNumeric
+ ? new RDExpression(left / right.NumericValue)
+ : new RDExpression(string.Format("{0}/({1})", left, right.ExpressionValue));
+ ///
+ public static RDExpression operator /(RDExpression left, RDExpression right) => left.IsNumeric && right.IsNumeric
+ ? new RDExpression(left.NumericValue / right.NumericValue)
+ : new RDExpression(string.Format("({0})/({1})", left.ExpressionValue, right.ExpressionValue));
+ ///
+ public static bool operator ==(RDExpression left, RDExpression right) => left.Equals(right);
+ ///
+ public static bool operator !=(RDExpression left, RDExpression right) => !(left == right);
+ ///
+ public static implicit operator RDExpression(float v) => new(v);
+ ///
+ public static implicit operator RDExpression(string v) => new(v);
+
+ static bool IComparisonOperators.operator >(RDExpression left, RDExpression right)
+ {
+ return left.CompareTo(right) > 0;
+ }
+
+ static bool IComparisonOperators.operator >=(RDExpression left, RDExpression right)
+ {
+ return left.CompareTo(right) >= 0;
+ }
+
+ static bool IComparisonOperators.operator <(RDExpression left, RDExpression right)
+ {
+ return left.CompareTo(right) < 0;
+ }
+
+ static bool IComparisonOperators.operator <=(RDExpression left, RDExpression right)
+ {
+ return left.CompareTo(right) <= 0;
+ }
+
+ static RDExpression IModulusOperators.operator %(RDExpression left, RDExpression right)
+ {
+ if (left.IsNumeric && right.IsNumeric)
+ {
+ return new RDExpression(left.NumericValue % right.NumericValue);
+ }
+ throw new NotImplementedException("Modulus operator is not implemented for non-numeric expressions.");
+ }
+
+ static RDExpression IDecrementOperators.operator --(RDExpression value)
+ {
+ if (value.IsNumeric)
+ {
+ return new RDExpression(value.NumericValue - 1);
+ }
+ throw new NotImplementedException("Decrement operator is not implemented for non-numeric expressions.");
+ }
+
+ static RDExpression IIncrementOperators.operator ++(RDExpression value)
+ {
+ if (value.IsNumeric)
+ {
+ return new RDExpression(value.NumericValue + 1);
+ }
+ throw new NotImplementedException("Increment operator is not implemented for non-numeric expressions.");
+ }
+
+ static RDExpression IUnaryNegationOperators.operator -(RDExpression value)
+ {
+ if (value.IsNumeric)
+ {
+ return new RDExpression(-value.NumericValue);
+ }
+ throw new NotImplementedException("Unary negation operator is not implemented for non-numeric expressions.");
+ }
+
+ static RDExpression IUnaryPlusOperators.operator +(RDExpression value)
+ {
+ return value;
+ }
+
+ private readonly string _exp = "";
+ ///
+ ///
+ ///
+ public bool IsNumeric { get; private set; } = false;
+ }
+}
diff --git a/RhythmBaseCore/Components/RDHit.cs b/RhythmBaseCore/Components/RDHit.cs
new file mode 100644
index 0000000..675553f
--- /dev/null
+++ b/RhythmBaseCore/Components/RDHit.cs
@@ -0,0 +1,51 @@
+using RhythmBase.Events;
+namespace RhythmBase.Components
+{
+ ///
+ /// Represents the moment a beat is hit in the rhythm game.
+ ///
+ public struct RDHit
+ {
+ ///
+ /// Gets the moment of pressing the beat.
+ ///
+ public RDBeat Beat { get; }
+ ///
+ /// Gets the length of time the player held the beat.
+ ///
+ public float Hold { get; }
+ ///
+ /// Gets the source event for this hit.
+ ///
+ public BaseBeat Parent { get; }
+ ///
+ /// Gets a value indicating whether this hit needs to be held down continuously.
+ ///
+ public readonly bool Holdable
+ {
+ get
+ {
+ return Hold > 0f;
+ }
+ }
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ /// The source event for this hit.
+ /// The moment of pressing the beat.
+ /// The length of time the player held the beat.
+ public RDHit(BaseBeat parent, RDBeat beat, float hold = 0f)
+ {
+ this = default;
+ Parent = parent;
+ Beat = beat;
+ Hold = hold;
+ }
+
+ ///
+ /// Returns a string that represents the current object.
+ ///
+ /// A string that represents the current object.
+ public override readonly string ToString() => string.Format("{{{0}, {1}}}", Beat, Parent);
+ }
+}
diff --git a/RhythmBaseCore/Components/RDLang/RDExpressionToken.cs b/RhythmBaseCore/Components/RDLang/RDExpressionToken.cs
new file mode 100644
index 0000000..24a97e4
--- /dev/null
+++ b/RhythmBaseCore/Components/RDLang/RDExpressionToken.cs
@@ -0,0 +1,52 @@
+using sly.lexer;
+
+namespace RhythmBase.Components.RDLang
+{
+ internal enum RDExpressionToken
+ {
+ [Lexeme(@"[0-9]+(\.[0-9]+)?")]
+ Number,
+ [Lexeme("[i][0-9]")]
+ VariableInt,
+ [Lexeme("[f][0-9]")]
+ VariableFloat,
+ [Lexeme("[b][0-9]")]
+ VariableBoolean,
+ [Lexeme(@"\+")]
+ Add,
+ [Lexeme("-")]
+ Subtract,
+ [Lexeme(@"\*")]
+ Multiply,
+ [Lexeme("/")]
+ Divide,
+ [Lexeme("true")]
+ True,
+ [Lexeme("false")]
+ False,
+ [Lexeme("=")]
+ Assignment,
+ [Lexeme(">=")]
+ GreaterThanOrEqual,
+ [Lexeme("<=")]
+ LessThanOrEqual,
+ [Lexeme(">")]
+ GreaterThan,
+ [Lexeme("<")]
+ LessThan,
+ [Lexeme(",")]
+ Comma,
+ [Lexeme(@"[A-Za-z0-9]+(?=\()")]
+ Identifier,
+ [Lexeme(@"\(")]
+ LeftParenthesis,
+ [Lexeme(@"\)")]
+ RightParenthesis,
+ [Lexeme(@"[A-Za-z0-9]+")]
+ PlainString,
+ [Lexeme(@"[A-Za-z0-9\+]+(?=\s*[\),])|""([A-Za-z0-9\+]+)""|str:([A-Za-z0-9\+]+)")]
+ String,
+ [Lexeme(@"[ \t\r\n]", IsSkippable = true)]
+ Whitespace,
+ }
+}
diff --git a/RhythmBaseCore/Components/RDLang/RDLang.cs b/RhythmBaseCore/Components/RDLang/RDLang.cs
new file mode 100644
index 0000000..ecaf7d9
--- /dev/null
+++ b/RhythmBaseCore/Components/RDLang/RDLang.cs
@@ -0,0 +1,47 @@
+using sly.parser;
+using sly.parser.generator;
+
+namespace RhythmBase.Components.RDLang
+{
+ ///
+ /// RDLang class provides functionality to parse and evaluate expressions written in a custom language.
+ ///
+ public static class RDLang
+ {
+ private static readonly RDVariables variables = new();
+ private static readonly RDLangParser parserInstance;
+ ///
+ /// Gets the collection of variables used in the custom language.
+ ///
+ ///
+ /// The collection of variables.
+ ///
+ public static RDVariables Variables { get => variables; }
+ ///
+ /// Lazy-initialized parser for the custom language expressions.
+ ///
+ internal static Lazy> Parser = new(() =>
+ {
+ var builder = new ParserBuilder();
+ var build = builder.BuildParser(parserInstance, ParserType.LL_RECURSIVE_DESCENT, "expression");
+ var parser = build.Result;
+ return parser;
+ });
+ static RDLang()
+ {
+ parserInstance = new RDLangParser() { Variables = variables };
+ }
+ ///
+ /// Tries to parse the given code string into a float result.
+ ///
+ /// The code string to parse.
+ /// The parsed float result if the parsing is successful.
+ /// True if parsing is successful; otherwise, false.
+ public static bool TryRun(string code, out float result)
+ {
+ var parseResult = Parser.Value.Parse(code);
+ result = parseResult.Result;
+ return parseResult.IsOk;
+ }
+ }
+}
diff --git a/RhythmBaseCore/Components/RDLang/RDLangParser.cs b/RhythmBaseCore/Components/RDLang/RDLangParser.cs
new file mode 100644
index 0000000..e1ab9e8
--- /dev/null
+++ b/RhythmBaseCore/Components/RDLang/RDLangParser.cs
@@ -0,0 +1,187 @@
+using sly.lexer;
+using sly.parser.generator;
+
+namespace RhythmBase.Components.RDLang
+{
+ internal class RDLangParser
+ {
+ internal const string PlainString = "";
+ private static List> TokenList { get; set; } = [];
+ public required RDVariables Variables { get; set; }
+ [NodeName("integer")]
+ [Production("primary: Number")]
+ public static float Primary(Token intToken) => float.Parse(intToken.Value);
+ [NodeName("boolean")]
+ [Production("primary: True")]
+ [Production("primary: False")]
+ public static float Boolean(Token token) => token.TokenID switch
+ {
+ RDExpressionToken.True => 1.0f,
+ RDExpressionToken.False => 0.0f,
+ _ => throw new InvalidOperationException("Invalid boolean token")
+ };
+ [NodeName("group")]
+ [Production("primary: LeftParenthesis [d] expression RightParenthesis [d]")]
+ public static float Group(float groupValue) => groupValue;
+ [NodeName("addOrSubstract")]
+ [Production("expression : term Add expression")]
+ [Production("expression : term Subtract expression")]
+ public static float Expression(float left, Token operatorToken, float right) => operatorToken.TokenID switch
+ {
+ RDExpressionToken.Add => left + right,
+ RDExpressionToken.Subtract => left - right,
+ _ => throw new InvalidOperationException("Invalid operator")
+ };
+ [NodeName("expression")]
+ [Production("expression : term")]
+ public static float Expression_Term(float termValue) => termValue;
+ [NodeName("multOrDivide")]
+ [Production("term : factor Multiply term")]
+ [Production("term : factor Divide term")]
+ public static float Term(float left, Token operatorToken, float right) => operatorToken.TokenID switch
+ {
+ RDExpressionToken.Multiply => left * right,
+ RDExpressionToken.Divide => left / right,
+ _ => throw new InvalidOperationException("Invalid operator")
+ };
+ [NodeName("term")]
+ [Production("term : factor")]
+ public static float TermFactor(float factorValue) => factorValue;
+ [NodeName("primary")]
+ [Production("factor : primary")]
+ public static float PrimaryFactor(float primValue) => primValue;
+ [NodeName("negate")]
+ [Production("factor : Add factor")]
+ [Production("factor : Subtract factor")]
+ public static float Factor(Token symbolToken, float factorValue) => symbolToken.TokenID switch
+ {
+ RDExpressionToken.Add => factorValue,
+ RDExpressionToken.Subtract => -factorValue,
+ _ => throw new InvalidOperationException("Invalid operator")
+ };
+ [NodeName("primary")]
+ [Production("primary: VariableInt")]
+ [Production("primary: VariableFloat")]
+ [Production("primary: VariableBoolean")]
+ [Production("primary: PlainString")]
+ public float PrimaryVariable(Token variableToken)
+ {
+ return variableToken.TokenID switch
+ {
+ RDExpressionToken.VariableInt => Variables.i[int.Parse(variableToken.Value[1..])],
+ RDExpressionToken.VariableFloat => Variables.f[int.Parse(variableToken.Value[1..])],
+ RDExpressionToken.VariableBoolean => Variables.b[int.Parse(variableToken.Value[1..])] ? 1.0f : 0.0f,
+ RDExpressionToken.PlainString => (float)Variables[variableToken.Value],
+ _ => throw new InvalidOperationException("Invalid variable type"),
+ };
+ }
+ [NodeName("string")]
+ [Production("argument: String")]
+ public static float String(Token stringToken)
+ {
+ TokenList.Add(new(RDExpressionToken.String,
+ stringToken.Value switch
+ {
+ ['s', 't', 'r', ':', ..] => stringToken.Value[4..],
+ ['"', .., '"'] => stringToken.Value[1..^1],
+ _ => stringToken.Value,
+ },
+ new()));
+ return 1;
+ }
+ [NodeName("compare")]
+ [Production("expression: term GreaterThanOrEqual expression")]
+ [Production("expression: term GreaterThan expression")]
+ [Production("expression: term LessThanOrEqual expression")]
+ [Production("expression: term LessThan expression")]
+ public static float Compare(float left, Token operatorToken, float right)
+ {
+ var result = operatorToken.TokenID switch
+ {
+ RDExpressionToken.GreaterThan => left > right,
+ RDExpressionToken.GreaterThanOrEqual => left >= right,
+ RDExpressionToken.LessThan => left < right,
+ RDExpressionToken.LessThanOrEqual => left <= right,
+ _ => throw new InvalidOperationException("Invalid operator")
+ };
+ return result ? 1.0f : 0.0f;
+ }
+ [NodeName("assignment")]
+ [Production("primary: VariableInt Assignment expression")]
+ [Production("primary: VariableFloat Assignment expression")]
+ [Production("primary: VariableBoolean Assignment expression")]
+ [Production("primary: PlainString Assignment expression")]
+ public float Assignment(Token nameToken, Token _, float expression)
+ {
+ var variableName = nameToken.Value;
+ switch (variableName[0])
+ {
+ case 'i':
+ Variables.i[int.Parse(variableName[1..])] = (int)expression;
+ break;
+ case 'f':
+ Variables.f[int.Parse(variableName[1..])] = expression;
+ break;
+ case 'b':
+ Variables.b[int.Parse(variableName[1..])] = expression != 0.0f;
+ break;
+ default:
+ Variables[variableName] = expression;
+ break;
+ }
+ return expression;
+ }
+ [NodeName("function_call")]
+ [Production("primary: Identifier LeftParenthesis [d] functionRight")]
+ public static float FunctionCallWithArguments(Token identifierToken, float _) => EvaluateFunction(identifierToken.Value, TokenList);
+ [NodeName("function_right")]
+ [Production("functionRight: RightParenthesis [d]")]
+ public static float FunctionCall() => 0;
+ [NodeName("function_right")]
+ [Production("functionRight: Number RightParenthesis [d]")]
+ [Production("functionRight: PlainString RightParenthesis [d]")]
+ public static float FunctionEnd(Token valueToken)
+ {
+ TokenList.Add(valueToken);
+ return 1;
+ }
+ [NodeName("arguments")]
+ [Production("functionRight: Number Comma [d] functionRight")]
+ [Production("functionRight: String Comma [d] functionRight")]
+ [Production("functionRight: PlainString Comma [d] functionRight")]
+ public static float ArgumentListWithComma(Token valueToken, float argument)
+ {
+ TokenList.Add(valueToken);
+ return 1 + argument;
+ }
+ [NodeName("arguments")]
+ [Production("functionRight: expression Comma [d] functionRight")]
+ public static float ArgumentListWithComma2(float arg, float argument)
+ {
+ TokenList.Add(new(RDExpressionToken.Number,arg.ToString(),new()));
+ return 1 + argument;
+ }
+ private static float EvaluateFunction(string functionName, List> tokenList)
+ {
+ return functionName switch
+ {
+ "Rand" => (float)(TokenList.Count == 1
+ ? int.TryParse(tokenList[0].Value, out int result)
+ ? RDVariables.Rand(result)
+ : throw new ArgumentException("Invalid argument type for Rand function")
+ : throw new ArgumentException("Invalid argument count for Rand function")),
+ "atLeastRank" => (TokenList.Count == 1
+ ? RDVariables.atLeastRank(tokenList[0].Value)
+ ? 1.0f
+ : 0.0f
+ : throw new ArgumentException("Invalid argument count for atLeastRank function")),
+ "atLeastNPerfects" => (TokenList.Count == 2
+ ? RDVariables.atLeastNPerfects(int.Parse(tokenList[0].Value), int.Parse(tokenList[1].Value))
+ ? 1.0f
+ : 0.0f
+ : throw new ArgumentException("Invalid argument count for atLeastNPerfects function")),
+ _ => throw new InvalidOperationException($"Unknown function: {functionName}"),
+ };
+ }
+ }
+}
diff --git a/RhythmBaseCore/Components/RDLevel.cs b/RhythmBaseCore/Components/RDLevel.cs
new file mode 100644
index 0000000..da1f68f
--- /dev/null
+++ b/RhythmBaseCore/Components/RDLevel.cs
@@ -0,0 +1,695 @@
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+using RhythmBase.Converters;
+using RhythmBase.Events;
+using RhythmBase.Exceptions;
+using RhythmBase.Extensions;
+using RhythmBase.Settings;
+using RhythmBase.Utils;
+
+using System.Diagnostics.CodeAnalysis;
+using System.IO.Compression;
+namespace RhythmBase.Components
+{
+ ///
+ /// Rhythm Doctor level.
+ ///
+ public class RDLevel : OrderedEventCollection, IDisposable
+ {
+ ///
+ /// The calculator that comes with the level.
+ ///
+ [JsonIgnore]
+ public BeatCalculator Calculator { get; }
+ ///
+ /// Level Settings.
+ ///
+ public Settings Settings { get; set; }
+ internal List ModifiableRows { get; } = new List(16);
+ internal List ModifiableDecorations { get; } = [];
+ ///
+ /// Level tile collection.
+ ///
+ public IReadOnlyList Rows => ModifiableRows.AsReadOnly();
+ ///
+ /// Level decoration collection.
+ ///
+ public IReadOnlyList Decorations => ModifiableDecorations.AsReadOnly();
+ ///
+ /// Level condition collection.
+ ///
+ public List Conditionals { get; }
+ ///
+ /// Level bookmark collection.
+ ///
+ public List Bookmarks { get; }
+ ///
+ /// Level colorPalette collection.
+ ///
+ public RDColor[] ColorPalette
+ {
+ get => colorPalette;
+ set => colorPalette = value.Length == 21 ? value : throw new RhythmBaseException();
+ }
+ ///
+ /// Level file path.
+ ///
+ [JsonIgnore]
+ public string Path => _path;
+ ///
+ /// Level directory path.
+ ///
+ [JsonIgnore]
+ public string Directory => System.IO.Path.GetDirectoryName(_path)!;
+ ///
+ /// Default beats with levels.
+ /// The beat is 1.
+ ///
+ [JsonIgnore]
+ public RDBeat DefaultBeat => Calculator.BeatOf(1f);
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public RDLevel()
+ {
+ _path = "";
+ Variables = new RDVariables();
+ Calculator = new BeatCalculator(this);
+ Settings = new Settings();
+ Conditionals = [];
+ Bookmarks = [];
+ ColorPalette = new RDColor[21];
+ }
+
+ ///
+ /// Initializes a new instance of the class with the specified items.
+ ///
+ /// The items to add to the level.
+ public RDLevel(IEnumerable items) : this()
+ {
+ foreach (IBaseEvent item in items)
+ Add(item);
+ }
+ ///
+ /// The default level within the game.
+ ///
+ public static RDLevel Default
+ {
+ get
+ {
+ RDLevel rdlevel = [];
+ rdlevel.ColorPalette =
+ [
+ RDColor.Black,
+ RDColor.White,
+ new(0xFF7F7F7Fu),
+ new(0xFFC3C3C3u),
+ new(0xFF880015u),
+ new(0xFFB97A57u),
+ RDColor.Red,
+
+ new(0xFFFFAEC9u),
+ new(0xFFFF7F27u),
+ new(0xFFFFC90Eu),
+ new(0xFFFFF200u),
+ new(0xFFEFE4B0u),
+ new(0xFF22B14Cu),
+ new(0xFFB5E61Du),
+
+ new(0xFF00A2E8u),
+ new(0xFF99D9EAu),
+ new(0xFF3F48CCu),
+ new(0xFF7092BEu),
+ new(0xFFA349A4u),
+ new(0xFFC8BFE7u),
+ new(0x00000000u)
+ ];
+
+ rdlevel.Settings.RankMaxMistakes = [
+ 20,
+ 15,
+ 10,
+ 5
+ ];
+
+ rdlevel.Settings.RankDescription = [
+ "Better call 911, now!",
+ "Ugh, you can do better",
+ "Not bad I guess...",
+ "We make a good team!",
+ "You are really good!",
+ "Wow! That's awesome!!"
+ ];
+
+ PlaySong playsong = new();
+ SetTheme settheme = new();
+ playsong.Song = new RDAudio() { Filename = "sndOrientalTechno" };
+ settheme.Preset = SetTheme.Theme.OrientalTechno;
+ rdlevel.AddRange([playsong, settheme]);
+ RowEventCollection samurai = rdlevel.CreateRow(new RDSingleRoom(RDRoomIndex.Room1), RDCharacters.Samurai);
+ samurai.Sound.Filename = "Shaker";
+ samurai.Add(new AddClassicBeat());
+ return rdlevel;
+ }
+ }
+ ///
+ /// Create a decoration and add it to the level.
+ ///
+ /// The room where this decoration is in.
+ /// The sprite referenced by this decoration.
+ /// Decoration that created and added to the level.
+ public DecorationEventCollection CreateDecoration(RDSingleRoom room, [NotNull] string sprite)
+ {
+ DecorationEventCollection temp = new(room)
+ {
+ Parent = this,
+ Filename = sprite
+ };
+ ModifiableDecorations.Add(temp);
+ return temp;
+ }
+ ///
+ /// Clone the decoration and add it to the level.
+ ///
+ /// Decoration that was copied.
+ ///
+ public DecorationEventCollection CloneDecoration(DecorationEventCollection decoration)
+ {
+ DecorationEventCollection temp = decoration.Clone();
+ ModifiableDecorations.Add(temp);
+ return temp;
+ }
+ ///
+ /// Remove the decoration from the level.
+ ///
+ /// The decoration to be removed.
+ ///
+ public bool RemoveDecoration(DecorationEventCollection decoration)
+ {
+ if (Decorations.Contains(decoration))
+ {
+ this.RemoveRange(decoration);
+ return ModifiableDecorations.Remove(decoration);
+ }
+ else
+ return false;
+ }
+ ///
+ /// Create a row and add it to the level.
+ ///
+ /// The room where this row is in.
+ /// The character used by this row.
+ /// Row that created and added to the level.
+ public RowEventCollection CreateRow(RDSingleRoom room, RDCharacter character)
+ {
+ RowEventCollection temp = new()
+ {
+ Character = character,
+ Rooms = room,
+ Parent = this
+ };
+ temp.Parent = this;
+ ModifiableRows.Add(temp);
+ return temp;
+ }
+ ///
+ /// Remove the row from the level.
+ ///
+ /// The row to be removed.
+ ///
+ public bool RemoveRow(RowEventCollection row)
+ {
+ if (Rows.Contains(row))
+ {
+ this.RemoveRange(row);
+ return ModifiableRows.Remove(row);
+ }
+ else
+ return false;
+ }
+ ///
+ /// Read from file as level.
+ /// Use default input settings.
+ /// Supports .rdlevel, .rdzip, .zip file extension.
+ ///
+ /// File path.
+ /// The minimum level version number supported by this library is 54.
+ ///
+ /// File not supported.
+ /// An instance of a level that reads from a file.
+ public static RDLevel Read(string filepath) => Read(filepath, new LevelReadOrWriteSettings());
+ ///
+ /// Read from file as level.
+ /// Supports .rdlevel, .rdzip, .zip file extension.
+ ///
+ /// File path.
+ /// Input settings.
+ /// The minimum level version number supported by this library is 54.
+ ///
+ /// File not supported.
+ /// An instance of a level that reads from a file.
+ public static RDLevel Read(string filepath, LevelReadOrWriteSettings settings)
+ {
+ JsonSerializer LevelSerializer = new();
+ LevelSerializer.Converters.Add(new RDLevelConverter(filepath, settings));
+ string extension = System.IO.Path.GetExtension(filepath);
+ RDLevel? Read;
+ if (extension != ".rdzip" && extension != ".zip")
+ {
+ if (extension != ".rdlevel")
+ throw new RhythmBaseException("File not supported.");
+ settings.OnBeforeReading();
+ Read = LevelSerializer.Deserialize(new JsonTextReader(File.OpenText(filepath)));
+ settings.OnAfterReading();
+ }
+ else
+ Read = ReadFromZip(filepath, settings);
+ return Read ?? [];
+ }
+ ///
+ /// Reads an RDLevel from a TextReader with the specified filepath and settings.
+ ///
+ /// The TextReader to read from.
+ /// The filepath of the RDLevel.
+ /// The settings to use for reading the RDLevel.
+ /// The deserialized RDLevel object.
+ /// Thrown when the file cannot be read.
+ public static RDLevel Read(TextReader reader, string filepath, LevelReadOrWriteSettings settings)
+ {
+ JsonSerializer LevelSerializer = new();
+ LevelSerializer.Converters.Add(new RDLevelConverter(filepath, settings));
+ RDLevel Read;
+ Read = LevelSerializer.Deserialize(new JsonTextReader(reader)) ?? throw new RhythmBaseException("Cannot read the file.");
+ return Read;
+ }
+
+ ///
+ /// Reads an RDLevel from a TextReader with the specified settings.
+ ///
+ /// The TextReader to read from.
+ /// The settings to use for reading the RDLevel.
+ /// The deserialized RDLevel object.
+ /// Thrown when the file cannot be read.
+ public static RDLevel Read(TextReader reader, LevelReadOrWriteSettings settings) => Read(reader, "", settings);
+ ///
+ /// Read from a zip file as a level.
+ ///
+ /// The path to the zip file.
+ /// An instance of RDLevel that reads from a zip file.
+ public static RDLevel ReadFromZip(string filepath) => ReadFromZip(filepath, new LevelReadOrWriteSettings());
+
+ ///
+ /// Read from a zip file as a level with specific settings.
+ ///
+ /// The path to the zip file.
+ /// The settings for reading the level.
+ /// An instance of RDLevel that reads from a zip file with specific settings.
+ public static RDLevel ReadFromZip(string filepath, LevelReadOrWriteSettings settings) => ReadFromZip(File.OpenRead(filepath), settings);
+ ///
+ /// Read from a zip file as a level.
+ ///
+ /// The stream of the zip file.
+ /// An instance of RDLevel that reads from a zip file.
+ public static RDLevel ReadFromZip(Stream stream) => ReadFromZip(stream, new LevelReadOrWriteSettings());
+
+ ///
+ /// Read from a zip file as a level with specific settings.
+ ///
+ /// The stream of the zip file.
+ /// The settings for reading the level.
+ /// An instance of RDLevel that reads from a zip file with specific settings.
+ public static RDLevel ReadFromZip(Stream stream, LevelReadOrWriteSettings settings)
+ {
+ DirectoryInfo tempDirectory = new(System.IO.Path.Combine(System.IO.Path.GetTempPath(), "RhythmBaseTemp_" + System.IO.Path.GetRandomFileName()));
+ tempDirectory.Create();
+ RDLevel ReadFromZip;
+ try
+ {
+ ZipFile.ExtractToDirectory(stream, tempDirectory.FullName);
+ ReadFromZip = Read(tempDirectory.GetFiles().Single(i => i.Extension == ".rdlevel").FullName, settings);
+ ReadFromZip.isZip = true;
+ }
+ catch (InvalidOperationException ex)
+ {
+ tempDirectory.Delete(true);
+ throw new RhythmBaseException("More than one RDLevel file has been found.", ex);
+ }
+ catch (Exception ex2)
+ {
+ tempDirectory.Delete(true);
+ throw new RhythmBaseException("Cannot extract the file.", ex2);
+ }
+ return ReadFromZip;
+ }
+ private JsonSerializer Serializer(LevelReadOrWriteSettings settings) => new()
+ {
+ Converters = { new RDLevelConverter(_path, settings) }
+ };
+ ///
+ /// Save the level.
+ /// Use default output settings.
+ ///
+ /// File path.
+ /// Overwriting is disabled by the settings and a file with the same name already exists.
+ public void Write(string filepath) => Write(filepath, new LevelReadOrWriteSettings());
+ ///
+ /// Save the level.
+ ///
+ /// File path.
+ /// Output settings.
+ /// Overwriting is disabled by the settings and a file with the same name already exists.
+ public void Write(string filepath, LevelReadOrWriteSettings settings)
+ {
+ using StreamWriter file = File.CreateText(filepath);
+ Write(file, settings);
+ }
+ ///
+ /// Save the level to a text writer.
+ /// Use default output settings.
+ ///
+ /// The text writer to write the level to.
+ public void Write(TextWriter stream) => Write(stream, new LevelReadOrWriteSettings());
+
+ ///
+ /// Save the level to a text writer.
+ ///
+ /// The text writer to write the level to.
+ /// The settings for writing the level.
+ public void Write(TextWriter stream, LevelReadOrWriteSettings settings)
+ {
+ using JsonTextWriter writer = new(stream);
+ settings.OnBeforeWriting();
+ Serializer(settings).Serialize(writer, this);
+ settings.OnAfterWriting();
+ }
+
+ ///
+ /// Save the level to a stream.
+ /// Use default output settings.
+ ///
+ /// The stream to write the level to.
+ public void Write(Stream stream) => Write(new StreamWriter(stream, leaveOpen: true), new LevelReadOrWriteSettings());
+
+ ///
+ /// Save the level to a stream.
+ ///
+ /// The stream to write the level to.
+ /// The settings for writing the level.
+ public void Write(Stream stream, LevelReadOrWriteSettings settings) => Write(new StreamWriter(stream), settings);
+ ///
+ /// Convert to JObject type.
+ ///
+ /// A JObject type that stores all the data for the level.
+ public JObject ToJObject() => ToJObject(new LevelReadOrWriteSettings());
+ ///
+ /// Convert to JObject type.
+ ///
+ /// A JObject type that stores all the data for the level.
+ public JObject ToJObject(LevelReadOrWriteSettings settings) => JObject.FromObject(this, new JsonSerializer
+ {
+ Converters = { new RDLevelConverter(Path, settings) }
+ });
+ ///
+ /// Convert to a string that can be read by the game.
+ /// Use default output settings.
+ ///
+ /// Level string.
+ public string ToRDLevelJson() => ToRDLevelJson(new LevelReadOrWriteSettings());
+ ///
+ /// Convert to a string that can be read by the game.
+ ///
+ /// Output settings.
+ /// Level string.
+ public string ToRDLevelJson(LevelReadOrWriteSettings settings)
+ {
+ StringWriter file = new();
+ Write(file, settings);
+ file.Close();
+ return file.ToString();
+ }
+ ///
+ /// Add event to the level.
+ ///
+ /// Event to be added.
+ ///
+ public override void Add(IBaseEvent item)
+ {
+ //添加默认节拍
+ if (((BaseEvent)item)._beat.IsEmpty)
+ ((BaseEvent)item)._beat._calculator = Calculator;
+ //部分事件只能在小节的开头
+ if (item is IBarBeginningEvent @event && ((BaseEvent)item)._beat.BarBeat.beat != 1f)
+ throw new IllegalBeatException(@event);
+ //更改节拍的关联关卡
+ ((BaseEvent)item)._beat._calculator = Calculator;
+ ((BaseEvent)item)._beat.ResetCache();
+ if (item.Type == EventType.Comment && ((Comment)item).Parent == null)
+ //注释事件可能在精灵板块,也可能不在
+ base.Add(item);
+ else if (item.Type == EventType.TintRows && ((TintRows)item).Parent == null)
+ base.Add(item);
+ else if (EventTypeUtils.RowTypes.Contains(item.Type))
+ {
+ BaseRowAction rowAction = (BaseRowAction)item;
+ if (rowAction.Parent == null)
+ throw new UnreadableEventException("The Parent property of this event should not be null. Call RowEventCollection.Add() instead.", item);
+ //添加至对应轨道
+ rowAction.Parent.AddSafely((BaseRowAction)item);
+ base.Add(item);
+ }
+ else if (EventTypeUtils.DecorationTypes.Contains(item.Type))
+ {
+ BaseDecorationAction decoAction = (BaseDecorationAction)item;
+ if (decoAction.Parent == null)
+ throw new UnreadableEventException("The Parent property of this event should not be null. Call DecorationEventCollection.Add() instead.", item);
+ //添加至对应精灵
+ decoAction.Parent.AddSafely((BaseDecorationAction)item);
+ base.Add(item);
+ }
+ //BPM 和 CPB
+ else if (item.Type == EventType.SetCrotchetsPerBar)
+ AddSetCrotchetsPerBar((SetCrotchetsPerBar)item);
+ else if (EventTypeUtils.ToEnums().Contains(item.Type))
+ AddBaseBeatsPerMinute((BaseBeatsPerMinute)item);
+ // 其他
+ else
+ base.Add(item);
+ }
+ ///
+ /// Determine if the level contains this event.
+ ///
+ /// Event.
+ ///
+ public override bool Contains(IBaseEvent item) => (Utils.EventTypeUtils.RowTypes.Contains(item.Type)
+ && Rows.Any((RowEventCollection i) => i.Contains(item))) || (Utils.EventTypeUtils.DecorationTypes.Contains(item.Type) && Decorations.Any((DecorationEventCollection i) => i.Contains(item))) || base.Contains(item);
+ ///
+ /// Remove event from the level.
+ ///
+ /// Event to be removed.
+ ///
+ public override bool Remove(IBaseEvent item)
+ {
+ bool Remove;
+ if (Utils.EventTypeUtils.RowTypes.Contains(item.Type)
+ && Rows.Any((RowEventCollection i) => i.RemoveSafely((BaseRowAction)item)))
+ {
+ base.Remove(item);
+ ((BaseEvent)item)._beat._calculator = null;
+ Remove = true;
+ }
+ else if (Utils.EventTypeUtils.DecorationTypes.Contains(item.Type)
+ && Decorations.Any((DecorationEventCollection i) => i.RemoveSafely((BaseDecorationAction)item)))
+ {
+ base.Remove(item);
+ ((BaseEvent)item)._beat._calculator = null;
+ Remove = true;
+ }
+ else if (Contains(item))
+ {
+ if (item.Type == EventType.SetCrotchetsPerBar)
+ Remove = RemoveSetCrotchetsPerBar((SetCrotchetsPerBar)item);
+ else if (Utils.EventTypeUtils.ToEnums().Contains(item.Type))
+ Remove = RemoveBaseBeatsPerMinute((BaseBeatsPerMinute)item);
+ else
+ {
+ bool result = base.Remove(item);
+ ((BaseEvent)item)._beat._calculator = null;
+ Remove = result;
+ }
+ }
+ else
+ Remove = false;
+ return Remove;
+ }
+ ///
+ /// Gets the status of the level at the specified beat.
+ ///
+ /// The beat at which to get the status.
+ /// A new instance of representing the status at the specified beat.
+ internal RDStatus GetStatus(RDBeat beat)
+ {
+ return new()
+ {
+ Beat = beat,
+ RoomStatus = [
+ new(){
+ Beat = beat,
+ RunningVFXs = this.Where(i=>i.Rooms.Contains(RDRoomIndex.Room1)&& i.VFXDuration().Contains(beat), new RDRange(null,beat))
+ },
+ new(){
+ Beat = beat,
+ RunningVFXs = this.Where(i=>i.Rooms.Contains(RDRoomIndex.Room2)&& i.VFXDuration().Contains(beat), new RDRange(null,beat))
+ },
+ new(){
+ Beat = beat,
+ RunningVFXs = this.Where(i=>i.Rooms.Contains(RDRoomIndex.Room3)&& i.VFXDuration().Contains(beat), new RDRange(null,beat))
+ },
+ new(){
+ Beat = beat,
+ RunningVFXs = this.Where(i=>i.Rooms.Contains(RDRoomIndex.Room4)&& i.VFXDuration().Contains(beat), new RDRange(null,beat))
+ },
+ new(){
+ Beat = beat,
+ RunningVFXs = this.Where(i=>i.Rooms.Contains(RDRoomIndex.RoomTop)&& i.VFXDuration().Contains(beat), new RDRange(null,beat))
+ }
+ ],
+ };
+ }
+ private void AddSetCrotchetsPerBar(SetCrotchetsPerBar item)
+ {
+ SetCrotchetsPerBar? frt = item.FrontOrDefault();
+ SetCrotchetsPerBar? nxt = item.NextOrDefault();
+ //更新拍号
+ RefreshCPBs(item._beat);
+ //添加事件
+ base.Add(item);
+ //更新计算器
+ Calculator.Refresh();
+ if (nxt != null)
+ {
+ BaseEvent? nxtE = item.After().FirstOrDefault((BaseEvent i) => i is IBarBeginningEvent &&
+ i.Type != EventType.SetCrotchetsPerBar &&
+ i._beat < nxt._beat);
+ float interval = ((nxtE != null) ? nxtE._beat.BeatOnly : nxt._beat.BeatOnly) - item._beat.BeatOnly;
+ float c = interval % item.CrotchetsPerBar;
+ if (c > 0f)
+ {
+ c = (c < 2f) ? (c + item.CrotchetsPerBar) : c;
+ base.Add(new SetCrotchetsPerBar
+ {
+ _beat = item._beat + interval - c,
+ _crotchetsPerBar = checked((uint)Math.Round((double)unchecked(c - 1f)))
+ });
+ }
+ else if (nxt.CrotchetsPerBar == item.CrotchetsPerBar)
+ base.Remove(nxt);
+ if (nxtE != null)
+ base.Add(new SetCrotchetsPerBar
+ {
+ _beat = nxtE._beat,
+ _crotchetsPerBar = frt?.CrotchetsPerBar ?? 8 - 1
+ });
+ }
+ // 更新计算器
+ Calculator.Refresh();
+ }
+ private bool RemoveSetCrotchetsPerBar(SetCrotchetsPerBar item)
+ {
+ SetCrotchetsPerBar? frt = item.FrontOrDefault();
+ SetCrotchetsPerBar? nxt = item.NextOrDefault();
+ if (nxt != null)
+ {
+ BaseEvent? nxtE = item.After().FirstOrDefault((BaseEvent i) => i is IBarBeginningEvent &&
+ i.Type != EventType.SetCrotchetsPerBar &&
+ i._beat < nxt._beat);
+ uint cpb = item.CrotchetsPerBar;
+ int interval = (int)((nxtE ?? nxt)._beat.BeatOnly - item._beat.BeatOnly);
+ long c = interval % frt?.CrotchetsPerBar ?? 8;
+ if (c > 0)
+ {
+ c = c < 2 ? c + item.CrotchetsPerBar : c;
+ if (c == nxt.CrotchetsPerBar)
+ base.Remove(nxt);
+ base.Add(new SetCrotchetsPerBar()
+ {
+ _beat = item._beat + interval - c,
+ _crotchetsPerBar = (uint)(c - 1)
+ });
+ }
+ else
+ {
+ if (nxtE != null && nxt.CrotchetsPerBar == (frt?.CrotchetsPerBar ?? 8))
+ {
+ base.Remove(nxt);
+ }
+ }
+ if (nxtE != null)
+ base.Add(new SetCrotchetsPerBar
+ {
+ _beat = nxtE._beat,
+ _crotchetsPerBar = ((frt != null) ? frt.CrotchetsPerBar : 8u - 1u)
+ });
+ Calculator.Refresh();
+ }
+ //更新计算器
+ Calculator.Refresh();
+ bool result = base.Remove(item);
+ RefreshCPBs(item.Beat);
+ item._beat._calculator = null;
+ Calculator.Refresh();
+ return result;
+ }
+ private void AddBaseBeatsPerMinute(BaseBeatsPerMinute item)
+ {
+ RefreshBPMs(item.Beat);
+ base.Add(item);
+ Calculator.Refresh();
+ }
+ private bool RemoveBaseBeatsPerMinute(BaseBeatsPerMinute item)
+ {
+ bool result = base.Remove(item);
+ Calculator.Refresh();
+ RefreshBPMs(item.Beat);
+ item._beat._calculator = null;
+ return result;
+ }
+ private void RefreshBPMs(RDBeat start)
+ {
+ foreach (var item in eventsBeatOrder.Keys)
+ item.ResetBPM();
+ foreach (var item in this.Where(i => i.Beat > start))
+ item.Beat.ResetBPM();
+ foreach (var item in Bookmarks)
+ item.Beat.ResetBPM();
+ }
+ private void RefreshCPBs(RDBeat start)
+ {
+ foreach (var item in eventsBeatOrder.Keys)
+ item.ResetCPB();
+ foreach (var item in this.Where(i => i.Beat > start))
+ item.Beat.ResetCPB();
+ foreach (var item in Bookmarks)
+ item.Beat.ResetCPB();
+ }
+ ///
+ public void Dispose()
+ {
+ if (isZip)
+ {
+ System.IO.Directory.Delete(Directory, true);
+ }
+ GC.SuppressFinalize(this);
+ }
+ ///
+ public override string ToString() => $"\"{Settings.Song}\" Count = {Count}";
+ internal string _path;
+ private bool isZip = false;
+ private RDColor[] colorPalette = new RDColor[21];
+
+ ///
+ /// Variables.
+ ///
+ [JsonIgnore]
+ public readonly RDVariables Variables;
+ }
+}
diff --git a/RhythmBaseCore/Components/RDPoint.cs b/RhythmBaseCore/Components/RDPoint.cs
new file mode 100644
index 0000000..931c0c4
--- /dev/null
+++ b/RhythmBaseCore/Components/RDPoint.cs
@@ -0,0 +1,175 @@
+using Newtonsoft.Json;
+using RhythmBase.Converters;
+using RhythmBase.Extensions;
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+namespace RhythmBase.Components
+{
+ ///
+ /// A point whose horizontal and vertical coordinates are nullable
+ ///
+ [JsonConverter(typeof(RDPointsConverter))]
+ [DebuggerDisplay($"{{{nameof(GetDebuggerDisplay)}(),nq}}")]
+ public struct RDPoint(float? x, float? y) : IRDVortex, IRDVortex
+ {
+ ///
+ /// Initializes a new instance of the struct with the specified size.
+ ///
+ /// The size to initialize the point with.
+ public RDPoint(RDSize sz) : this(sz.Width, sz.Height) { }
+
+ ///
+ /// Gets a value indicating whether this point is empty.
+ ///
+ public readonly bool IsEmpty => X == null && Y == null;
+
+ ///
+ /// Gets or sets the X coordinate of the point.
+ ///
+ public float? X { get; set; } = x;
+
+ ///
+ /// Gets or sets the Y coordinate of the point.
+ ///
+ public float? Y { get; set; } = y;
+
+ ///
+ /// Offsets the point by the specified point.
+ ///
+ /// The point to offset by.
+ public void Offset(RDPoint p)
+ {
+ X += p.X;
+ Y += p.Y;
+ }
+
+ ///
+ /// Offsets the point by the specified amounts.
+ ///
+ /// The amount to offset the X coordinate.
+ /// The amount to offset the Y coordinate.
+ public void Offset(float? dx, float? dy)
+ {
+ X += dx;
+ Y += dy;
+ }
+
+ ///
+ /// Adds the specified size to the point.
+ ///
+ /// The point to add to.
+ /// The size to add.
+ /// A new point that is the result of the addition.
+ public static RDPoint Add(RDPoint pt, RDSizeI sz) => new(
+ pt.X + sz.Width, pt.Y + sz.Height
+ );
+
+ ///
+ /// Adds the specified size to the point.
+ ///
+ /// The point to add to.
+ /// The size to add.
+ /// A new point that is the result of the addition.
+ public static RDPoint Add(RDPoint pt, RDSize sz) => new(
+ pt.X + sz.Width, pt.Y + sz.Height
+ );
+
+ ///
+ /// Subtracts the specified size from the point.
+ ///
+ /// The point to subtract from.
+ /// The size to subtract.
+ /// A new point that is the result of the subtraction.
+ public static RDPoint Subtract(RDPoint pt, RDSizeI sz) => new(
+ pt.X - sz.Width, pt.Y - sz.Height
+ );
+
+ ///
+ /// Subtracts the specified size from the point.
+ ///
+ /// The point to subtract from.
+ /// The size to subtract.
+ /// A new point that is the result of the subtraction.
+ public static RDPoint Subtract(RDPoint pt, RDSize sz) => new(
+ pt.X - sz.Width, pt.Y - sz.Height
+ );
+
+ ///
+ /// Multiplies the point by the specified matrix.
+ ///
+ /// The matrix to multiply by.
+ /// A new point that is the result of the multiplication.
+ /// Thrown when the matrix is not a 2x2 matrix.
+ public readonly RDPoint MultipyByMatrix(float[,] matrix)
+ {
+ if (matrix.Rank == 2 && matrix.Length == 4)
+ {
+ RDPoint MultipyByMatrix = new(X * matrix[0, 0] + Y * matrix[1, 0], X * matrix[0, 1] + Y * matrix[1, 1]);
+ return MultipyByMatrix;
+ }
+ throw new Exception("Matrix not match, 2*2 matrix expected.");
+ }
+ ///
+ /// Rotate.
+ ///
+ public readonly RDPoint Rotate(float angle)
+ {
+ float[,] array = new float[2, 2];
+ array[0, 0] = (float)Math.Cos((double)angle);
+ array[0, 1] = (float)Math.Sin((double)angle);
+ array[1, 0] = (float)-(float)Math.Sin((double)angle);
+ array[1, 1] = (float)Math.Cos((double)angle);
+ return MultipyByMatrix(array);
+ }
+ ///
+ /// Rotate at a given pivot.
+ ///
+ /// Given pivot.
+ /// Angle.
+ ///
+ public readonly RDPoint Rotate(RDPointN pivot, float angle) => (this - new RDSizeN(pivot)).Rotate(angle) + new RDSizeN(pivot);
+ ///
+ public override readonly bool Equals([NotNullWhen(true)] object? obj) => obj is RDPoint e && Equals(e);
+ ///
+ public override readonly int GetHashCode()
+ {
+ HashCode h = default;
+ h.Add(X);
+ h.Add(Y);
+ return h.ToHashCode();
+ }
+ ///
+ public override readonly string ToString() => $"[{X?.ToString() ?? "null"}, {Y?.ToString() ?? "null"}]";
+ ///
+ public readonly bool Equals(RDPoint other) => other.X.NullableEquals(X) && other.Y.NullableEquals(Y);
+ ///
+ public static RDPoint operator +(RDPoint pt, RDSizeI sz) => Add(pt, sz);
+ ///
+ public static RDPoint operator +(RDPoint pt, RDSize sz) => Add(pt, sz);
+ ///
+ public static RDPoint operator -(RDPoint pt, RDSizeI sz) => Subtract(pt, sz);
+ ///
+ public static RDPoint operator -(RDPoint pt, RDSize sz) => Subtract(pt, sz);
+ ///
+ public static RDPoint operator *(RDPoint pt, float? x) => new(pt.X * x, pt.Y * x);
+ ///
+ public static RDPoint operator /(RDPoint pt, float? x) => new(pt.X / x, pt.Y / x);
+ ///
+ public static bool operator ==(RDPoint left, RDPoint right) => left.Equals(right);
+ ///
+ public static bool operator !=(RDPoint left, RDPoint right) => !left.Equals(right);
+ ///
+ /// Implicitly converts an to an .
+ ///
+ /// The to convert.
+ /// A new with the same coordinates as the input .
+ public static implicit operator RDPointE(RDPoint p) => new(p.X, p.Y);
+ ///
+ /// Explicitly converts an to an .
+ ///
+ /// The to convert.
+ /// A new with the same coordinates as the input .
+ public static explicit operator RDSize(RDPoint p) => new(p.X, p.Y);
+ private readonly string GetDebuggerDisplay() => ToString();
+ }
+}
diff --git a/RhythmBaseCore/Components/RDPointE.cs b/RhythmBaseCore/Components/RDPointE.cs
new file mode 100644
index 0000000..9055539
--- /dev/null
+++ b/RhythmBaseCore/Components/RDPointE.cs
@@ -0,0 +1,237 @@
+using Newtonsoft.Json;
+using RhythmBase.Converters;
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+namespace RhythmBase.Components
+{
+ ///
+ /// A point whose horizontal and vertical coordinates are nullable
+ ///
+ [JsonConverter(typeof(RDPointsConverter))]
+ [DebuggerDisplay($"{{{nameof(GetDebuggerDisplay)}(),nq}}")]
+ public struct RDPointE(RDExpression? x, RDExpression? y) :
+ IRDVortex,
+ IRDVortex,
+ IRDVortex
+ {
+ ///
+ /// Initializes a new instance of the struct with the specified size.
+ ///
+ /// The size to initialize the point with.
+ public RDPointE(RDSize sz) : this(sz.Width, sz.Height) { }
+
+ ///
+ /// Initializes a new instance of the struct with the specified coordinates.
+ ///
+ /// The x-coordinate.
+ /// The y-coordinate.
+ public RDPointE(float x, float y) : this((RDExpression)x, (RDExpression)y) { }
+
+ ///
+ /// Initializes a new instance of the struct with the specified x-coordinate and y-coordinate.
+ ///
+ /// The x-coordinate.
+ /// The y-coordinate.
+ public RDPointE(RDExpression? x, float y) : this(x, (RDExpression)y) { }
+
+ ///
+ /// Initializes a new instance of the struct with the specified x-coordinate and y-coordinate.
+ ///
+ /// The x-coordinate.
+ /// The y-coordinate.
+ public RDPointE(float x, RDExpression? y) : this((RDExpression)x, y) { }
+
+ ///
+ /// Initializes a new instance of the struct with the specified x-coordinate and y-coordinate.
+ ///
+ /// The x-coordinate.
+ /// The y-coordinate.
+ public RDPointE(string x, float y) : this((RDExpression)x, (RDExpression)y) { }
+
+ ///
+ /// Initializes a new instance of the struct with the specified x-coordinate and y-coordinate.
+ ///
+ /// The x-coordinate.
+ /// The y-coordinate.
+ public RDPointE(float x, string y) : this((RDExpression)x, (RDExpression)y) { }
+
+ ///
+ /// Initializes a new instance of the struct with the specified x-coordinate and y-coordinate.
+ ///
+ /// The x-coordinate.
+ /// The y-coordinate.
+ public RDPointE(string x, RDExpression? y) : this((RDExpression)x, y) { }
+
+ ///
+ /// Initializes a new instance of the struct with the specified x-coordinate and y-coordinate.
+ ///
+ /// The x-coordinate.
+ /// The y-coordinate.
+ public RDPointE(RDExpression? x, string y) : this(x, (RDExpression)y) { }
+
+ ///
+ /// Initializes a new instance of the struct with the specified x-coordinate and y-coordinate.
+ ///
+ /// The x-coordinate.
+ /// The y-coordinate.
+ public RDPointE(string x, string y) : this((RDExpression)x, (RDExpression)y) { }
+
+ ///
+ /// Initializes a new instance of the struct with the specified point.
+ ///
+ /// The point to initialize the point with.
+ public RDPointE(RDPointI p) : this(p.X, p.Y) { }
+
+ ///
+ /// Initializes a new instance of the struct with the specified point.
+ ///
+ /// The point to initialize the point with.
+ public RDPointE(RDPoint p) : this(p.X, p.Y) { }
+ ///
+ public readonly bool IsEmpty => X == null && Y == null;
+
+ ///
+ public RDExpression? X { get; set; } = x;
+
+ ///
+ public RDExpression? Y { get; set; } = y;
+
+ ///
+ /// Offsets the point by the specified point.
+ ///
+ /// The point to offset by.
+ public void Offset(RDPoint p)
+ {
+ X += p.X;
+ Y += p.Y;
+ }
+
+ ///
+ /// Offsets the point by the specified amounts.
+ ///
+ /// The amount to offset the x-coordinate.
+ /// The amount to offset the y-coordinate.
+ public void Offset(float? dx, float? dy)
+ {
+ X += dx;
+ Y += dy;
+ }
+
+ ///
+ /// Adds the specified size to the point.
+ ///
+ /// The point to add to.
+ /// The size to add.
+ /// The resulting point.
+ public static RDPointE Add(RDPointE pt, RDSizeI sz) => new(
+ pt.X + sz.Width, pt.Y + sz.Height
+ );
+
+ ///
+ public static RDPointE Add(RDPointE pt, RDSize sz) => new(
+ pt.X + sz.Width, pt.Y + sz.Height
+ );
+
+ ///
+ public static RDPointE Add(RDPointE pt, RDSizeE sz) => new(
+ pt.X + sz.Width, pt.Y + sz.Height
+ );
+
+ ///
+ /// Subtracts the specified size from the point.
+ ///
+ /// The point to subtract from.
+ /// The size to subtract.
+ /// The resulting point.
+ public static RDPointE Subtract(RDPointE pt, RDSizeI sz) => new(
+ pt.X - sz.Width, pt.Y - sz.Height
+ );
+
+ ///
+ public static RDPointE Subtract(RDPointE pt, RDSize sz) => new(
+ pt.X - sz.Width, pt.Y - sz.Height
+ );
+
+ ///
+ public static RDPointE Subtract(RDPointE pt, RDSizeE sz) => new(
+ pt.X - sz.Width, pt.Y - sz.Height
+ );
+
+ ///
+ public override readonly bool Equals([NotNullWhen(true)] object? obj) => obj is RDPointE e && Equals(e);
+
+ ///
+ public override readonly int GetHashCode() => HashCode.Combine(X, Y);
+
+ ///
+ public override readonly string ToString() => $"[{(X?.ExpressionValue) ?? "null"},{(Y?.ExpressionValue) ?? "null"}]";
+
+ ///
+ public readonly bool Equals(RDPointE other) => other.X == X && other.Y == Y;
+
+ ///
+ /// Multiplies the point by the specified matrix.
+ ///
+ /// The matrix to multiply by.
+ /// The resulting point.
+ /// Thrown when the matrix is not 2x2.
+ public readonly RDPointE MultipyByMatrix(RDExpression[,] matrix)
+ {
+ if (matrix.Rank == 2 && matrix.Length == 4)
+ {
+ RDPointE MultipyByMatrix = new(
+ X * matrix[0, 0] + Y * matrix[1, 0],
+ X * matrix[0, 1] + Y * matrix[1, 1]);
+ return MultipyByMatrix;
+ }
+ throw new Exception("Matrix not match, 2*2 matrix expected.");
+ }
+ ///
+ /// Rotate.
+ ///
+ public readonly RDPointE Rotate(float angle)
+ {
+ RDExpression[,] array = new RDExpression[2, 2];
+ array[0, 0] = (float)Math.Cos((double)angle);
+ array[0, 1] = (float)Math.Sin((double)angle);
+ array[1, 0] = (float)-(float)Math.Sin((double)angle);
+ array[1, 1] = (float)Math.Cos((double)angle);
+ return MultipyByMatrix(array);
+ }
+ ///
+ /// Rotate at a given pivot.
+ ///
+ /// Given pivot.
+ /// Angle.
+ ///
+ public readonly RDPointE Rotate(RDPointE pivot, float angle) => (this - new RDSizeE(pivot)).Rotate(angle) + new RDSizeE(pivot);
+ ///
+ public static RDPointE operator +(RDPointE pt, RDSizeI sz) => Add(pt, sz);
+ ///
+ public static RDPointE operator +(RDPointE pt, RDSize sz) => Add(pt, sz);
+ ///
+ public static RDPointE operator +(RDPointE pt, RDSizeE sz) => Add(pt, sz);
+ ///
+ public static RDPointE operator -(RDPointE pt, RDSizeI sz) => Subtract(pt, sz);
+ ///
+ public static RDPointE operator -(RDPointE pt, RDSize sz) => Subtract(pt, sz);
+ ///
+ public static RDPointE operator -(RDPointE pt, RDSizeE sz) => Subtract(pt, sz);
+ ///
+ public static RDPointE operator *(RDPointE pt, RDExpression x) => new(pt.X * x, pt.Y * x);
+ ///
+ public static RDPointE operator /(RDPointE pt, RDExpression x) => new(pt.X / x, pt.Y / x);
+ ///
+ public static bool operator ==(RDPointE left, RDPointE right) => left.Equals(right);
+ ///
+ public static bool operator !=(RDPointE left, RDPointE right) => !left.Equals(right);
+
+ ///
+ /// Converts the specified to an .
+ ///
+ /// The point to convert.
+ /// An that represents the converted point.
+ public static explicit operator RDSizeE(RDPointE p) => new(p);
+ private readonly string GetDebuggerDisplay() => ToString();
+ }
+}
diff --git a/RhythmBaseCore/Components/RDPointI.cs b/RhythmBaseCore/Components/RDPointI.cs
new file mode 100644
index 0000000..2dc2827
--- /dev/null
+++ b/RhythmBaseCore/Components/RDPointI.cs
@@ -0,0 +1,201 @@
+using Newtonsoft.Json;
+using RhythmBase.Converters;
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+namespace RhythmBase.Components
+{
+ ///
+ /// A point whose horizontal and vertical coordinates are nullable
+ ///
+ [JsonConverter(typeof(RDPointsConverter))]
+ [DebuggerDisplay($"{{{nameof(GetDebuggerDisplay)}(),nq}}")]
+ public struct RDPointI(int? x, int? y) : IRDVortex
+ {
+ ///
+ /// Initializes a new instance of the struct with the specified size.
+ ///
+ /// The size to initialize the point with.
+ public RDPointI(RDSizeI sz) : this(sz.Width, sz.Height) { }
+
+ ///
+ /// Initializes a new instance of the struct with the specified nullable size.
+ ///
+ /// The nullable size to initialize the point with.
+ public RDPointI(RDSizeN sz) : this(
+ (int)Math.Round((double)sz.Width),
+ (int)Math.Round((double)sz.Height))
+ { }
+
+ ///
+ /// Gets a value indicating whether this point is empty.
+ ///
+ public readonly bool IsEmpty => X == null && Y == null;
+
+ ///
+ /// Gets or sets the X coordinate of this point.
+ ///
+ public int? X { get; set; } = x;
+
+ ///
+ /// Gets or sets the Y coordinate of this point.
+ ///
+ public int? Y { get; set; } = y;
+
+ ///
+ /// Offsets this point by the specified point.
+ ///
+ /// The point to offset by.
+ public void Offset(RDPointI p)
+ {
+ X += p.X;
+ Y += p.Y;
+ }
+
+ ///
+ /// Offsets this point by the specified amounts.
+ ///
+ /// The amount to offset the X coordinate.
+ /// The amount to offset the Y coordinate.
+ public void Offset(int? dx, int? dy)
+ {
+ X += dx;
+ Y += dy;
+ }
+
+ ///
+ /// Returns a new point that is the ceiling of the specified point.
+ ///
+ /// The point to ceiling.
+ /// A new point that is the ceiling of the specified point.
+ public static RDPointI Ceiling(RDPoint value) => new(
+ (value.X == null) ? null : (int)Math.Ceiling((double)value.X),
+ (value.Y == null) ? null : (int)Math.Ceiling((double)value.Y)
+ );
+
+ ///
+ /// Adds the specified size to the specified point.
+ ///
+ /// The point to add to.
+ /// The size to add.
+ /// A new point that is the sum of the specified point and size.
+ public static RDPointI Add(RDPointI pt, RDSizeI sz) => new(
+ pt.X + sz.Width, pt.Y + sz.Height
+ );
+
+ ///
+ /// Returns a new point that is the truncated value of the specified point.
+ ///
+ /// The point to truncate.
+ /// A new point that is the truncated value of the specified point.
+ public static RDPointI Truncate(RDPoint value) => new(
+ (value.X == null) ? null : (int)Math.Truncate((double)value.X),
+ (value.Y == null) ? null : (int)Math.Truncate((double)value.Y)
+ );
+
+ ///
+ /// Subtracts the specified size from the specified point.
+ ///
+ /// The point to subtract from.
+ /// The size to subtract.
+ /// A new point that is the difference of the specified point and size.
+ public static RDPointI Subtract(RDPointI pt, RDSizeI sz) => new(
+ pt.X - sz.Width, pt.Y - sz.Height
+ );
+
+ ///
+ /// Returns a new point that is the rounded value of the specified point.
+ ///
+ /// The point to round.
+ /// A new point that is the rounded value of the specified point.
+ public static RDPointI Round(RDPoint value) => new(
+ ((value.X == null) ? null : (int)Math.Round((double)value.X.Value)),
+ ((value.Y == null) ? null : (int)Math.Round((double)value.Y.Value))
+ );
+
+ ///
+ /// Multiplies this point by the specified matrix.
+ ///
+ /// The matrix to multiply by.
+ /// A new point that is the result of the multiplication.
+ /// Thrown when the matrix is not a 2x2 matrix.
+ public readonly RDPoint MultipyByMatrix(float[,] matrix)
+ {
+ if (matrix.Rank == 2 && matrix.Length == 4)
+ {
+ int? num = X;
+ float? num2 = (num == null ? null : num) * matrix[0, 0];
+ num = Y;
+ float? x = num2 + ((num == null ? null : num) * matrix[1, 0]);
+ num = X;
+ float? num3 = (num == null ? null : num) * matrix[0, 1];
+ num = Y;
+ RDPoint MultipyByMatrix = new(x, num3 + ((num != null) ? num : null) * matrix[1, 1]);
+ return MultipyByMatrix;
+ }
+ throw new Exception("Matrix not match, 2*2 matrix expected.");
+ }
+
+ ///
+ /// Rotates this point by the specified angle.
+ ///
+ /// The angle to rotate by.
+ /// A new point that is the result of the rotation.
+ public readonly RDPoint Rotate(float angle)
+ {
+ float[,] array = new float[2, 2];
+ array[0, 0] = (float)Math.Cos((double)angle);
+ array[0, 1] = (float)Math.Sin((double)angle);
+ array[1, 0] = (float)-(float)Math.Sin((double)angle);
+ array[1, 1] = (float)Math.Cos((double)angle);
+ return MultipyByMatrix(array);
+ }
+ ///
+ /// Rotate at a given pivot.
+ ///
+ /// Given pivot.
+ /// Angle.
+ ///
+ public readonly RDPoint Rotate(RDPointN pivot, float angle) => ((RDPoint)this - new RDSizeN(pivot)).Rotate(angle) + new RDSizeN(pivot);
+ ///
+ public override readonly bool Equals([NotNullWhen(true)] object? obj) => obj is RDPointI e && Equals(e);
+ ///
+ public override readonly int GetHashCode() => HashCode.Combine(X, Y);
+ ///
+ public override readonly string ToString() => $"[{(X?.ToString()) ?? "null"},{(Y?.ToString()) ?? "null"}]";
+ ///
+ public readonly bool Equals(RDPointI other) => other.X == X && other.Y == Y;
+ ///
+ public static RDPointI operator +(RDPointI pt, RDSizeI sz) => Add(pt, sz);
+ ///
+ public static RDPointI operator -(RDPointI pt, RDSizeI sz) => Subtract(pt, sz);
+ ///
+ public static RDPointI operator *(RDPointI pt, int? x) => new(pt.X * x, pt.Y * x);
+ ///
+ public static RDPointI operator /(RDPointI pt, int? x) => new(pt.X / x, pt.Y / x);
+ ///
+ public static bool operator ==(RDPointI left, RDPointI right) => left.Equals(right);
+ ///
+ public static bool operator !=(RDPointI left, RDPointI right) => !left.Equals(right);
+ ///
+ /// Implicitly converts an to an .
+ ///
+ /// The to convert.
+ /// A new with the same coordinates as the input .
+ public static implicit operator RDPoint(RDPointI p) => new(p.X, p.Y);
+
+ ///
+ /// Implicitly converts an to an .
+ ///
+ /// The to convert.
+ /// A new with the same coordinates as the input .
+ public static implicit operator RDPointE(RDPointI p) => new(p.X, p.Y);
+
+ ///
+ /// Explicitly converts an to an .
+ ///
+ /// The to convert.
+ /// A new with the same dimensions as the input .
+ public static explicit operator RDSizeI(RDPointI p) => new(p);
+ private readonly string GetDebuggerDisplay() => ToString();
+ }
+}
diff --git a/RhythmBaseCore/Components/RDPointN.cs b/RhythmBaseCore/Components/RDPointN.cs
new file mode 100644
index 0000000..2b547d1
--- /dev/null
+++ b/RhythmBaseCore/Components/RDPointN.cs
@@ -0,0 +1,196 @@
+using Newtonsoft.Json;
+using RhythmBase.Converters;
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+namespace RhythmBase.Components
+{
+ ///
+ /// A point whose horizontal and vertical coordinates are non-nullable
+ ///
+ [JsonConverter(typeof(RDPointsConverter))]
+ [DebuggerDisplay($"{{{nameof(GetDebuggerDisplay)}(),nq}}")]
+ public struct RDPointN(float x, float y) : IRDVortex, IRDVortex
+ {
+ ///
+ /// Initializes a new instance of the struct with the specified size.
+ ///
+ /// The size to initialize the point with.
+ public RDPointN(RDSizeN sz) : this(sz.Width, sz.Height) { }
+
+ ///
+ /// Gets or sets the X coordinate of the point.
+ ///
+ public float X { get; set; } = x;
+
+ ///
+ /// Gets or sets the Y coordinate of the point.
+ ///
+ public float Y { get; set; } = y;
+
+ ///
+ /// Offsets the point by the specified size.
+ ///
+ /// The size to offset the point by.
+ public void Offset(RDSizeN p)
+ {
+ X += p.Width;
+ Y += p.Height;
+ }
+
+ ///
+ /// Offsets the point by the specified point.
+ ///
+ /// The point to offset the point by.
+ public void Offset(RDPointN p)
+ {
+ X += p.X;
+ Y += p.Y;
+ }
+
+ ///
+ /// Offsets the point by the specified horizontal and vertical amounts.
+ ///
+ /// The horizontal amount to offset the point by.
+ /// The vertical amount to offset the point by.
+ public void Offset(float dx, float dy)
+ {
+ X += dx;
+ Y += dy;
+ }
+
+ ///
+ /// Adds the specified size to the point.
+ ///
+ /// The point to add to.
+ /// The size to add.
+ /// A new point that is the result of the addition.
+ public static RDPointN Add(RDPointN pt, RDSizeNI sz)
+ {
+ RDPointN Add = new(pt.X + (float)sz.Width, pt.Y + (float)sz.Height);
+ return Add;
+ }
+
+ ///
+ /// Adds the specified size to the point.
+ ///
+ /// The point to add to.
+ /// The size to add.
+ /// A new point that is the result of the addition.
+ public static RDPointN Add(RDPointN pt, RDSizeN sz)
+ {
+ RDPointN Add = new(pt.X + sz.Width, pt.Y + sz.Height);
+ return Add;
+ }
+
+ ///
+ /// Subtracts the specified size from the point.
+ ///
+ /// The point to subtract from.
+ /// The size to subtract.
+ /// A new point that is the result of the subtraction.
+ public static RDPointN Subtract(RDPointN pt, RDSizeNI sz)
+ {
+ RDPointN Subtract = new(pt.X - (float)sz.Width, pt.Y - (float)sz.Height);
+ return Subtract;
+ }
+
+ ///
+ /// Subtracts the specified size from the point.
+ ///
+ /// The point to subtract from.
+ /// The size to subtract.
+ /// A new point that is the result of the subtraction.
+ public static RDPointN Subtract(RDPointN pt, RDSizeN sz)
+ {
+ RDPointN Subtract = new(pt.X - sz.Width, pt.Y - sz.Height);
+ return Subtract;
+ }
+
+ ///
+ /// Multiplies the point by the specified 2x2 matrix.
+ ///
+ /// The 2x2 matrix to multiply the point by.
+ /// A new point that is the result of the multiplication.
+ /// Thrown when the matrix is not a 2x2 matrix.
+ public readonly RDPointN MultipyByMatrix(float[,] matrix)
+ {
+ if (matrix.Rank == 2 && matrix.Length == 4)
+ {
+ RDPointN MultipyByMatrix = new(X * matrix[0, 0] + Y * matrix[1, 0], X * matrix[0, 1] + Y * matrix[1, 1]);
+ return MultipyByMatrix;
+ }
+ throw new Exception("Matrix not match, 2*2 matrix expected.");
+ }
+ ///
+ /// Rotate.
+ ///
+ public readonly RDPointN Rotate(float angle)
+ {
+ float[,] array = new float[2, 2];
+ array[0, 0] = (float)Math.Cos((double)angle);
+ array[0, 1] = (float)Math.Sin((double)angle);
+ array[1, 0] = (float)-(float)Math.Sin((double)angle);
+ array[1, 1] = (float)Math.Cos((double)angle);
+ return MultipyByMatrix(array);
+ }
+ ///
+ /// Rotate at a given pivot.
+ ///
+ /// Given pivot.
+ /// Angle.
+ ///
+ public readonly RDPointN Rotate(RDPointN pivot, float angle) => (this - new RDSizeN(pivot)).Rotate(angle) + new RDSizeN(pivot);
+ ///
+ public override readonly bool Equals([NotNullWhen(true)] object? obj) => obj is RDPointN e && Equals(e);
+ ///
+ public override readonly int GetHashCode()
+ {
+ HashCode h = default;
+ h.Add(X);
+ h.Add(Y);
+ return h.ToHashCode();
+ }
+ ///
+ public override readonly string ToString() => $"[{X}, {Y}]";
+ ///
+ public readonly bool Equals(RDPointN other) => other.X == X && other.Y == Y;
+ ///
+ public static RDPointN operator +(RDPointN pt, RDSizeNI sz) => Add(pt, sz);
+ ///
+ public static RDPointN operator +(RDPointN pt, RDSizeN sz) => Add(pt, sz);
+ ///
+ public static RDPointN operator -(RDPointN pt, RDSizeNI sz) => Subtract(pt, sz);
+ ///
+ public static RDPointN operator -(RDPointN pt, RDSizeN sz) => Subtract(pt, sz);
+ ///
+ public static RDPointN operator *(RDPointN pt, float x) => new(pt.X * x, pt.Y * x);
+ ///
+ public static RDPointN operator /(RDPointN pt, float x) => new(pt.X / x, pt.Y / x);
+ ///
+ public static bool operator ==(RDPointN left, RDPointN right) => left.Equals(right);
+ ///
+ public static bool operator !=(RDPointN left, RDPointN right) => !left.Equals(right);
+
+ ///
+ /// Implicitly converts an to an .
+ ///
+ /// The to convert.
+ /// A new with the same coordinates.
+ public static implicit operator RDPoint(RDPointN p) => new(new float?(p.X), new float?(p.Y));
+
+ ///
+ /// Implicitly converts an to an .
+ ///
+ /// The to convert.
+ /// A new with the same coordinates.
+ public static implicit operator RDPointE(RDPointN p) => new(p.X, p.Y);
+
+ ///
+ /// Explicitly converts an to an .
+ ///
+ /// The to convert.
+ /// A new with the same dimensions.
+ public static explicit operator RDSizeN(RDPointN p) => new(p.X, p.Y);
+ private readonly string GetDebuggerDisplay() => ToString();
+ }
+}
diff --git a/RhythmBaseCore/Components/RDPointNI.cs b/RhythmBaseCore/Components/RDPointNI.cs
new file mode 100644
index 0000000..dcbb423
--- /dev/null
+++ b/RhythmBaseCore/Components/RDPointNI.cs
@@ -0,0 +1,200 @@
+using Newtonsoft.Json;
+using RhythmBase.Converters;
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+namespace RhythmBase.Components
+{
+ ///
+ /// A point whose horizontal and vertical coordinates are non-nullable
+ ///
+ [JsonConverter(typeof(RDPointsConverter))]
+ [DebuggerDisplay($"{{{nameof(GetDebuggerDisplay)}(),nq}}")]
+ public struct RDPointNI(int x, int y) : IRDVortex
+ {
+ ///
+ /// Initializes a new instance of the struct with the specified size.
+ ///
+ /// The size to initialize the point with.
+ public RDPointNI(RDSizeNI sz) : this(sz.Width, sz.Height) { }
+
+ ///
+ /// Gets or sets the X coordinate of the point.
+ ///
+ public int X { get; set; } = x;
+
+ ///
+ /// Gets or sets the Y coordinate of the point.
+ ///
+ public int Y { get; set; } = y;
+
+ ///
+ /// Offsets the point by the specified point.
+ ///
+ /// The point to offset by.
+ public void Offset(RDPointNI p)
+ {
+ X += p.X;
+ Y += p.Y;
+ }
+
+ ///
+ /// Offsets the point by the specified size.
+ ///
+ /// The size to offset by.
+ public void Offset(RDSizeNI p)
+ {
+ X += p.Width;
+ Y += p.Height;
+ }
+
+ ///
+ /// Offsets the point by the specified horizontal and vertical amounts.
+ ///
+ /// The horizontal offset.
+ /// The vertical offset.
+ public void Offset(int dx, int dy)
+ {
+ X += dx;
+ Y += dy;
+ }
+
+ ///
+ /// Returns a new point with coordinates rounded up to the nearest integer values.
+ ///
+ /// The point to round up.
+ /// A new point with coordinates rounded up.
+ public static RDPointNI Ceiling(RDPointN value) => new(
+ (int)Math.Ceiling((double)value.X),
+ (int)Math.Ceiling((double)value.Y)
+ );
+
+ ///
+ /// Adds the specified size to the point.
+ ///
+ /// The point to add to.
+ /// The size to add.
+ /// A new point with the size added.
+ public static RDPointNI Add(RDPointNI pt, RDSizeNI sz) => new(
+ pt.X + sz.Width, pt.Y + sz.Height
+ );
+
+ ///
+ /// Returns a new point with coordinates truncated to the nearest integer values.
+ ///
+ /// The point to truncate.
+ /// A new point with coordinates truncated.
+ public static RDPointNI Truncate(RDPointN value) => new(
+ (int)Math.Truncate((double)value.X),
+ (int)Math.Truncate((double)value.Y)
+ );
+
+ ///
+ /// Subtracts the specified size from the point.
+ ///
+ /// The point to subtract from.
+ /// The size to subtract.
+ /// A new point with the size subtracted.
+ public static RDPointNI Subtract(RDPointNI pt, RDSizeNI sz) => new(
+ pt.X - sz.Width, pt.Y - sz.Height
+ );
+
+ ///
+ /// Returns a new point with coordinates rounded to the nearest integer values.
+ ///
+ /// The point to round.
+ /// A new point with coordinates rounded.
+ public static RDPointNI Round(RDPointN value) => new(
+ (int)Math.Round((double)value.X),
+ (int)Math.Round((double)value.Y)
+ );
+
+ ///
+ /// Multiplies the point by the specified 2x2 matrix.
+ ///
+ /// The 2x2 matrix to multiply by.
+ /// A new point resulting from the matrix multiplication.
+ /// Thrown when the matrix is not a 2x2 matrix.
+ public readonly RDPointN MultipyByMatrix(float[,] matrix)
+ {
+ if (matrix.Rank == 2 && matrix.Length == 4)
+ {
+ RDPointN MultipyByMatrix = new(X * matrix[0, 0] + (Y * matrix[1, 0]), (X * matrix[0, 1]) + Y * matrix[1, 1]);
+ return MultipyByMatrix;
+ }
+ throw new Exception("Matrix not match, 2*2 matrix expected.");
+ }
+ ///
+ /// Rotate.
+ ///
+ public readonly RDPointN Rotate(float angle)
+ {
+ float[,] array = new float[2, 2];
+ array[0, 0] = (float)Math.Cos((double)angle);
+ array[0, 1] = (float)Math.Sin((double)angle);
+ array[1, 0] = -(float)Math.Sin((double)angle);
+ array[1, 1] = (float)Math.Cos((double)angle);
+ return MultipyByMatrix(array);
+ }
+ ///
+ /// Rotate at a given pivot.
+ ///
+ /// Given pivot.
+ /// Angle.
+ ///
+ public readonly RDPointN Rotate(RDPointN pivot, float angle) => ((RDPointN)this - new RDSizeN(pivot)).Rotate(angle) + new RDSizeN(pivot);
+ ///
+ public override readonly bool Equals([NotNullWhen(true)] object? obj) => obj is RDPointNI e && Equals(e);
+ ///
+ public override readonly int GetHashCode()
+ {
+ HashCode h = default;
+ h.Add(X);
+ h.Add(Y);
+ return h.ToHashCode();
+ }
+ ///
+ public override readonly string ToString() => $"[{X}, {Y}]";
+ ///
+ public readonly bool Equals(RDPointNI other) => other.X == X && other.Y == Y;
+ ///
+ public static RDPointNI operator +(RDPointNI pt, RDSizeNI sz) => Add(pt, sz);
+ ///
+ public static RDPointNI operator -(RDPointNI pt, RDSizeNI sz) => Subtract(pt, sz);
+ ///
+ public static RDPointNI operator *(RDPointNI pt, int x) => new(pt.X * x, pt.Y * x);
+ ///
+ public static RDPointNI operator /(RDPointNI pt, int x) => new(pt.X / x, pt.Y / x);
+ ///
+ public static bool operator ==(RDPointNI left, RDPointNI right) => left.Equals(right);
+ ///
+ public static bool operator !=(RDPointNI left, RDPointNI right) => !left.Equals(right);
+ ///
+ /// Implicitly converts an to an .
+ ///
+ /// The to convert.
+ /// An with the same coordinates.
+ public static implicit operator RDPointN(RDPointNI p) => new(p.X, p.Y);
+
+ ///
+ /// Implicitly converts an to an .
+ ///
+ /// The to convert.
+ /// An with the same coordinates.
+ public static implicit operator RDPointI(RDPointNI p) => new(new int?(p.X), new int?(p.Y));
+
+ ///
+ /// Implicitly converts an to an .
+ ///
+ /// The to convert.
+ /// An with the same coordinates.
+ public static implicit operator RDPointE(RDPointNI p) => new(p.X, p.Y);
+
+ ///
+ /// Explicitly converts an to an .
+ ///
+ /// The to convert.
+ /// An with the same dimensions.
+ public static explicit operator RDSizeNI(RDPointNI p) => new(p.X, p.Y);
+ private readonly string GetDebuggerDisplay() => ToString();
+ }
+}
diff --git a/RhythmBaseCore/Components/RDRange.cs b/RhythmBaseCore/Components/RDRange.cs
new file mode 100644
index 0000000..533eafa
--- /dev/null
+++ b/RhythmBaseCore/Components/RDRange.cs
@@ -0,0 +1,92 @@
+using RhythmBase.Exceptions;
+namespace RhythmBase.Components
+{
+ ///
+ /// Beat range.
+ ///
+ public struct RDRange
+ {
+ ///
+ /// Start beat.
+ ///
+ public RDBeat? Start { get; }
+ ///
+ /// End beat.
+ ///
+ public RDBeat? End { get; }
+ ///
+ /// Beat interval.
+ ///
+ public readonly float BeatInterval
+ {
+ get
+ {
+ bool flag = Start != null && End != null;
+ float BeatInterval;
+ if (flag)
+ {
+ BeatInterval = End!.Value.BeatOnly - Start!.Value.BeatOnly;
+ }
+ else
+ {
+ BeatInterval = float.PositiveInfinity;
+ }
+ return BeatInterval;
+ }
+ }
+ ///
+ /// Time interval.
+ ///
+ ///
+ public readonly TimeSpan TimeInterval
+ {
+ get
+ {
+ bool flag = Start != null && End != null;
+ TimeSpan TimeInterval;
+ if (flag)
+ {
+ if (Start!.Value.BeatOnly == End!.Value.BeatOnly)
+ {
+ TimeInterval = TimeSpan.Zero;
+ }
+ else
+ {
+ TimeInterval = End.Value.TimeSpan - Start.Value.TimeSpan;
+ }
+ }
+ else
+ {
+ TimeInterval = TimeSpan.MaxValue;
+ }
+ return TimeInterval;
+ }
+ }
+ /// Start beat.
+ /// End beat.
+ public RDRange(RDBeat? start, RDBeat? end)
+ {
+ this = default;
+ if (start != null && end != null && !((RDBeat)start).FromSameLevelOrNull((RDBeat)end))
+ {
+ throw new RhythmBaseException("RDIndexes must come from the same RDLevel.");
+ }
+ if (start != null && end != null && start > end)
+ {
+ Start = end;
+ End = start;
+ }
+ else
+ {
+ Start = start;
+ End = end;
+ }
+ }
+ ///
+ /// Determines whether the specified beat is within the range.
+ ///
+ /// The beat to check.
+ /// True if the beat is within the range; otherwise, false.
+ public readonly bool Contains(RDBeat b) => (Start == null || Start < b) && (End == null || b < End);
+ }
+}
diff --git a/RhythmBaseCore/Components/RDRect.cs b/RhythmBaseCore/Components/RDRect.cs
new file mode 100644
index 0000000..e1c0344
--- /dev/null
+++ b/RhythmBaseCore/Components/RDRect.cs
@@ -0,0 +1,268 @@
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+namespace RhythmBase.Components
+{
+ ///
+ /// Represents a rectangle defined by its left, top, right, and bottom edges.
+ ///
+ [DebuggerDisplay($"{{{nameof(GetDebuggerDisplay)}(),nq}}")]
+ public struct RDRect(float? left, float? top, float? right, float? bottom) : IEquatable
+ {
+ ///
+ /// Gets or sets the left edge of the rectangle.
+ ///
+ public float? Left { get; set; } = left;
+
+ ///
+ /// Gets or sets the right edge of the rectangle.
+ ///
+ public float? Right { get; set; } = right;
+
+ ///
+ /// Gets or sets the top edge of the rectangle.
+ ///
+ public float? Top { get; set; } = top;
+
+ ///
+ /// Gets or sets the bottom edge of the rectangle.
+ ///
+ public float? Bottom { get; set; } = bottom;
+
+ ///
+ /// Gets the point at the left-bottom corner of the rectangle.
+ ///
+ public readonly RDPoint LeftBottom { get => new(Left, Bottom); }
+
+ ///
+ /// Gets the point at the right-bottom corner of the rectangle.
+ ///
+ public readonly RDPoint RightBottom { get => new(Right, Bottom); }
+
+ ///
+ /// Gets the point at the left-top corner of the rectangle.
+ ///
+ public readonly RDPoint LeftTop { get => new(Left, Top); }
+
+ ///
+ /// Gets the point at the right-top corner of the rectangle.
+ ///
+ public readonly RDPoint RightTop { get => new(Right, Top); }
+
+ ///
+ /// Gets the width of the rectangle.
+ ///
+ public readonly float? Width => Right - Left;
+
+ ///
+ /// Gets the height of the rectangle.
+ ///
+ public readonly float? Height => Top - Bottom;
+
+ ///
+ /// Initializes a new instance of the struct with the specified location and size.
+ ///
+ /// The location of the rectangle.
+ /// The size of the rectangle.
+ public RDRect(RDPoint? location, RDSize? size) : this(location?.X, location?.Y + size?.Height, location?.X + size?.Width, location?.Y) { }
+
+ ///
+ /// Initializes a new instance of the struct with the specified size.
+ ///
+ /// The size of the rectangle.
+ public RDRect(RDSize size) : this(new float?(0f), size.Height, size.Width, new float?(0f)) { }
+
+ ///
+ /// Initializes a new instance of the struct with the specified width and height.
+ ///
+ /// The width of the rectangle.
+ /// The height of the rectangle.
+ public RDRect(float? width, float? height) : this(new float?(0f), height, width, new float?(0f)) { }
+
+ ///
+ /// Gets the location of the rectangle.
+ ///
+ public readonly RDPoint Location => new(Left, Bottom);
+
+ ///
+ /// Gets the size of the rectangle.
+ ///
+ public readonly RDSize Size => new(Width, Height);
+
+ ///
+ /// Inflates the specified rectangle by the specified size.
+ ///
+ /// The rectangle to inflate.
+ /// The size to inflate by.
+ /// The inflated rectangle.
+ public static RDRect Inflate(RDRect rect, RDSize size)
+ {
+ RDRect result = new(rect.Left, rect.Top, rect.Right, rect.Bottom);
+ result.Inflate(size);
+ return result;
+ }
+
+ ///
+ /// Inflates the specified rectangle by the specified width and height.
+ ///
+ /// The rectangle to inflate.
+ /// The width to inflate by.
+ /// The height to inflate by.
+ /// The inflated rectangle.
+ public static RDRect Inflate(RDRect rect, float? x, float? y)
+ {
+ RDRect result = new(rect.Left, rect.Top, rect.Right, rect.Bottom);
+ result.Inflate(x, y);
+ return result;
+ }
+
+ ///
+ /// Returns the union of two rectangles.
+ ///
+ /// The first rectangle.
+ /// The second rectangle.
+ /// The union of the two rectangles.
+ public static RDRect Union(RDRect rect1, RDRect rect2)
+ {
+ RDRect Union = new(new float?((rect1.Left == null || rect2.Left == null) ? 0f : Math.Min(rect1.Left.Value, rect2.Left.Value)), new float?((rect1.Top == null || rect2.Top == null) ? 0f : Math.Min(rect1.Top.Value, rect2.Top.Value)), new float?((rect1.Right == null || rect2.Right == null) ? 0f : Math.Min(rect1.Right.Value, rect2.Right.Value)), new float?((rect1.Bottom == null || rect2.Bottom == null) ? 0f : Math.Min(rect1.Bottom.Value, rect2.Bottom.Value)));
+ return Union;
+ }
+
+ ///
+ /// Returns the intersection of two rectangles.
+ ///
+ /// The first rectangle.
+ /// The second rectangle.
+ /// The intersection of the two rectangles.
+ public static RDRect Intersect(RDRect rect1, RDRect rect2) => rect1.IntersectsWithInclusive(rect2) ? new RDRect(new float?((rect1.Left == null || rect2.Left == null) ? 0f : Math.Max(rect1.Left.Value, rect2.Left.Value)), new float?((rect1.Top == null || rect2.Top == null) ? 0f : Math.Max(rect1.Top.Value, rect2.Top.Value)), new float?((rect1.Right == null || rect2.Right == null) ? 0f : Math.Min(rect1.Right.Value, rect2.Right.Value)), new float?((rect1.Bottom == null || rect2.Bottom == null) ? 0f : Math.Min(rect1.Bottom.Value, rect2.Bottom.Value))) : default;
+
+ ///
+ /// Truncates the edges of the specified rectangle.
+ ///
+ /// The rectangle to truncate.
+ /// The truncated rectangle.
+ public static RDRect Truncate(RDRect rect)
+ {
+ RDRect Truncate = new(
+ rect.Left == null ? null : (float)Math.Truncate((double)rect.Left),
+ rect.Top == null ? null : (float)Math.Truncate((double)rect.Top),
+ rect.Right == null ? null : (float)Math.Truncate((double)rect.Right),
+ rect.Bottom == null ? null : (float)Math.Truncate((double)rect.Bottom));
+ return Truncate;
+ }
+
+ ///
+ /// Offsets the rectangle by the specified width and height.
+ ///
+ /// The width to offset by.
+ /// The height to offset by.
+ public void Offset(float? x, float? y)
+ {
+ Left += x;
+ Top += y;
+ Right += x;
+ Bottom += y;
+ }
+
+ ///
+ /// Offsets the rectangle by the specified point.
+ ///
+ /// The point to offset by.
+ public void Offset(RDPoint p) => Offset(p.X, p.Y);
+
+ ///
+ /// Inflates the rectangle by the specified size.
+ ///
+ /// The size to inflate by.
+ public void Inflate(RDSize size)
+ {
+ Left -= size.Width;
+ Top += size.Height;
+ Right += size.Width;
+ Bottom -= size.Height;
+ }
+
+ ///
+ /// Inflates the rectangle by the specified width and height.
+ ///
+ /// The width to inflate by.
+ /// The height to inflate by.
+ public void Inflate(float? width, float? height)
+ {
+ Left -= width;
+ Top += height;
+ Right += width;
+ Bottom -= height;
+ }
+
+ ///
+ /// Determines whether the rectangle contains the specified point.
+ ///
+ /// The x-coordinate of the point.
+ /// The y-coordinate of the point.
+ /// true if the rectangle contains the point; otherwise, false.
+ public readonly bool Contains(float? x, float? y) => Left < x && x < Right && Bottom < y && y < Top;
+
+ ///
+ /// Determines whether the rectangle contains the specified point.
+ ///
+ /// The point to check.
+ /// true if the rectangle contains the point; otherwise, false.
+ public readonly bool Contains(RDPoint p) => Contains(p.X, p.Y);
+
+ ///
+ /// Determines whether the rectangle contains the specified rectangle.
+ ///
+ /// The rectangle to check.
+ /// true if the rectangle contains the specified rectangle; otherwise, false.
+ public readonly bool Contains(RDRect rect) => Left < rect.Left && rect.Right < Right && Bottom < rect.Bottom && rect.Top < Top;
+
+ ///
+ /// Returns the union of this rectangle and the specified rectangle.
+ ///
+ /// The rectangle to union with.
+ /// The union of the two rectangles.
+ public readonly RDRect Union(RDRect rect) => Union(this, rect);
+
+ ///
+ /// Returns the intersection of this rectangle and the specified rectangle.
+ ///
+ /// The rectangle to intersect with.
+ /// The intersection of the two rectangles.
+ public readonly object Intersect(RDRect rect) => Intersect(this, rect);
+
+ ///
+ /// Determines whether this rectangle intersects with the specified rectangle.
+ ///
+ /// The rectangle to check.
+ /// true if the rectangles intersect; otherwise, false.
+ public readonly bool IntersectsWith(RDRect rect) => Left < rect.Right && Right > rect.Left && Top < rect.Bottom && Bottom > rect.Top;
+
+ ///
+ /// Determines whether this rectangle intersects with the specified rectangle, including edges.
+ ///
+ /// The rectangle to check.
+ /// true if the rectangles intersect; otherwise, false.
+ public readonly bool IntersectsWithInclusive(RDRect rect) => Left <= rect.Right && Right >= rect.Left && Top <= rect.Bottom && Bottom >= rect.Top;
+
+ ///
+ public static bool operator ==(RDRect rect1, RDRect rect2) => rect1.Equals(rect2);
+
+ ///
+ public static bool operator !=(RDRect rect1, RDRect rect2) => !rect1.Equals(rect2);
+ ///
+ public override readonly bool Equals([NotNullWhen(true)] object? obj) => obj is RDRect e && Equals(e);
+ ///
+ public override readonly int GetHashCode() => HashCode.Combine(Left, Top, Right, Bottom);
+ ///
+ public override readonly string ToString() => $"{{Location=[{Left?.ToString() ?? "null"},{Bottom?.ToString() ?? "null"}],Size=[{Width?.ToString() ?? "null"},{Height?.ToString() ?? "null"}]}}";
+ ///
+ public readonly bool Equals(RDRect other) => Left == other.Left && Top == other.Top && Right == other.Right && Bottom == other.Bottom;
+ ///
+ /// Implicitly converts an to an .
+ ///
+ /// The to convert.
+ /// The converted .
+ public static implicit operator RDRectE(RDRect rect) => new(rect.Left, rect.Top, rect.Right, rect.Bottom);
+ private readonly string GetDebuggerDisplay() => ToString();
+ }
+}
\ No newline at end of file
diff --git a/RhythmBaseCore/Components/RDRectE.cs b/RhythmBaseCore/Components/RDRectE.cs
new file mode 100644
index 0000000..125e4d8
--- /dev/null
+++ b/RhythmBaseCore/Components/RDRectE.cs
@@ -0,0 +1,179 @@
+using System.Diagnostics.CodeAnalysis;
+namespace RhythmBase.Components
+{
+ ///
+ /// Represents a rectangle defined by four expressions: left, top, right, and bottom.
+ ///
+ /// The left expression of the rectangle.
+ /// The top expression of the rectangle.
+ /// The right expression of the rectangle.
+ /// The bottom expression of the rectangle.
+ public struct RDRectE(RDExpression? left, RDExpression? top, RDExpression? right, RDExpression? bottom) : IEquatable
+ {
+ ///
+ /// Gets or sets the left expression of the rectangle.
+ ///
+ public RDExpression? Left { get; set; } = left;
+
+ ///
+ /// Gets or sets the right expression of the rectangle.
+ ///
+ public RDExpression? Right { get; set; } = right;
+
+ ///
+ /// Gets or sets the top expression of the rectangle.
+ ///
+ public RDExpression? Top { get; set; } = top;
+
+ ///
+ /// Gets or sets the bottom expression of the rectangle.
+ ///
+ public RDExpression? Bottom { get; set; } = bottom;
+
+ ///
+ /// Gets the left-bottom point of the rectangle.
+ ///
+ public readonly RDPointE LeftBottom => new(Left, Bottom);
+
+ ///
+ /// Gets the right-bottom point of the rectangle.
+ ///
+ public readonly RDPointE RightBottom => new(Right, Bottom);
+
+ ///
+ /// Gets the left-top point of the rectangle.
+ ///
+ public readonly RDPointE LeftTop => new(Left, Top);
+
+ ///
+ /// Gets the right-top point of the rectangle.
+ ///
+ public readonly RDPointE RightTop => new(Right, Top);
+
+ ///
+ /// Gets the width of the rectangle.
+ ///
+ public readonly RDExpression? Width => Right - Left;
+
+ ///
+ /// Gets the height of the rectangle.
+ ///
+ public readonly RDExpression? Height => Top - Bottom;
+
+ ///
+ /// Initializes a new instance of the struct with the specified location and size.
+ ///
+ /// The location of the rectangle.
+ /// The size of the rectangle.
+ public RDRectE(RDPointE? location, RDSizeE? size) : this(location?.X, location?.Y + size?.Height, location?.X + size?.Width, location?.Y) { }
+
+ ///
+ /// Initializes a new instance of the struct with the specified size.
+ ///
+ /// The size of the rectangle.
+ public RDRectE(RDSizeE size) : this(new RDExpression?(0f), size.Height, size.Width, new RDExpression?(0f)) { }
+
+ ///
+ /// Initializes a new instance of the struct with the specified width and height.
+ ///
+ /// The width of the rectangle.
+ /// The height of the rectangle.
+ public RDRectE(RDExpression? width, RDExpression? height) : this(new RDExpression?(0f), height, width, new RDExpression?(0f)) { }
+
+ ///
+ /// Gets the location of the rectangle.
+ ///
+ public readonly RDPointE Location => new(Left, Bottom);
+
+ ///
+ /// Gets the size of the rectangle.
+ ///
+ public readonly RDSizeE Size => new(Width, Height);
+
+ ///
+ /// Inflates the specified rectangle by the specified size.
+ ///
+ /// The rectangle to inflate.
+ /// The size to inflate by.
+ /// The inflated rectangle.
+ public static RDRectE Inflate(RDRectE rect, RDSizeE size)
+ {
+ RDRectE result = new(rect.Left, rect.Top, rect.Right, rect.Bottom);
+ result.Inflate(size);
+ return result;
+ }
+ ///
+ /// Inflates the specified rectangle by the specified width and height.
+ ///
+ /// The rectangle to inflate.
+ /// The width to inflate by.
+ /// The height to inflate by.
+ /// The inflated rectangle.
+ public static RDRectE Inflate(RDRectE rect, RDExpression? x, RDExpression? y)
+ {
+ RDRectE result = new(rect.Left, rect.Top, rect.Right, rect.Bottom);
+ result.Inflate(x, y);
+ return result;
+ }
+
+ ///
+ /// Truncates the specified rectangle.
+ ///
+ /// The rectangle to truncate.
+ /// The truncated rectangle.
+ public static RDRectE Truncate(RDRectE rect) => new(rect.Left, rect.Top, rect.Right, rect.Bottom);
+
+ ///
+ /// Offsets the rectangle by the specified width and height.
+ ///
+ /// The width to offset by.
+ /// The height to offset by.
+ public void Offset(RDExpression? x, RDExpression? y)
+ {
+ Left += x;
+ Top += y;
+ Right += x;
+ Bottom += y;
+ }
+ ///
+ /// Offsets the rectangle by the specified point.
+ ///
+ /// The point to offset by.
+ public void Offset(RDPointE p) => Offset(p.X, p.Y);
+ ///
+ /// Inflates the rectangle by the specified size.
+ ///
+ /// The size to inflate by.
+ public void Inflate(RDSizeE size)
+ {
+ Left -= size.Width;
+ Top += size.Height;
+ Right += size.Width;
+ Bottom -= size.Height;
+ }
+ ///
+ /// Inflates the rectangle by the specified width and height.
+ ///
+ /// The width to inflate by.
+ /// The height to inflate by.
+ public void Inflate(RDExpression? width, RDExpression? height)
+ {
+ Left -= width;
+ Top += height;
+ Right += width;
+ Bottom -= height;
+ }
+ ///
+ public static bool operator ==(RDRectE rect1, RDRectE rect2) => rect1.Equals(rect2);
+ ///
+ public static bool operator !=(RDRectE rect1, RDRectE rect2) => !rect1.Equals(rect2);
+ ///
+ public override readonly bool Equals([NotNullWhen(true)] object? obj) => obj is RDRectE e && Equals(e);
+ ///
+ public override readonly int GetHashCode() => HashCode.Combine(Left, Top, Right, Bottom);
+ ///
+ public override readonly string ToString() => $"{{Location=[{Left},{Bottom}],Size=[{Width},{Height}]}}";
+ ///
+ public readonly bool Equals(RDRectE other) => Left == other.Left && Top == other.Top && Right == other.Right && Bottom == other.Bottom;
+ }
+}
diff --git a/RhythmBaseCore/Components/RDRectI.cs b/RhythmBaseCore/Components/RDRectI.cs
new file mode 100644
index 0000000..2da32cf
--- /dev/null
+++ b/RhythmBaseCore/Components/RDRectI.cs
@@ -0,0 +1,287 @@
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+namespace RhythmBase.Components
+{
+ ///
+ /// Represents a rectangle structure containing left, top, right, and bottom boundary values.
+ ///
+ /// The left boundary of the rectangle.
+ /// The top boundary of the rectangle.
+ /// The right boundary of the rectangle.
+ /// The bottom boundary of the rectangle.
+ [DebuggerDisplay($"{{{nameof(GetDebuggerDisplay)}(),nq}}")]
+ public struct RDRectI(int? left, int? top, int? right, int? bottom) : IEquatable
+ {
+ ///
+ /// Gets or sets the left boundary of the rectangle.
+ ///
+ public int? Left { get; set; } = left;
+
+ ///
+ /// Gets or sets the right boundary of the rectangle.
+ ///
+ public int? Right { get; set; } = right;
+
+ ///
+ /// Gets or sets the top boundary of the rectangle.
+ ///
+ public int? Top { get; set; } = top;
+
+ ///
+ /// Gets or sets the bottom boundary of the rectangle.
+ ///
+ public int? Bottom { get; set; } = bottom;
+
+ ///
+ /// Gets the bottom-left corner point of the rectangle.
+ ///
+ public readonly RDPointI LeftBottom { get => new(Left, Bottom); }
+
+ ///
+ /// Gets the bottom-right corner point of the rectangle.
+ ///
+ public readonly RDPointI RightBottom { get => new(Right, Bottom); }
+
+ ///
+ /// Gets the top-left corner point of the rectangle.
+ ///
+ public readonly RDPointI LeftTop { get => new(Left, Top); }
+
+ ///
+ /// Gets the top-right corner point of the rectangle.
+ ///
+ public readonly RDPointI RightTop { get => new(Right, Top); }
+
+ ///
+ /// Gets the width of the rectangle.
+ ///
+ public readonly int? Width => checked(Right - Left);
+
+ ///
+ /// Gets the height of the rectangle.
+ ///
+ public readonly int? Height => checked(Top - Bottom);
+
+ ///
+ /// Initializes a new instance of the rectangle with the specified location and size.
+ ///
+ /// The location of the rectangle.
+ /// The size of the rectangle.
+ public RDRectI(RDPointI? location, RDSizeI? size) : this(location?.X, location?.Y + size?.Height, location?.X + size?.Width, location?.Y) { }
+
+ ///
+ /// Initializes a new instance of the rectangle with the specified size.
+ ///
+ /// The size of the rectangle.
+ public RDRectI(RDSizeI? size) : this(0, size?.Height, size?.Width, 0) { }
+
+ ///
+ /// Initializes a new instance of the rectangle with the specified width and height.
+ ///
+ /// The width of the rectangle.
+ /// The height of the rectangle.
+ public RDRectI(int? width, int? height) : this(0, height, width, 0) { }
+ ///
+ /// Gets the location of the rectangle.
+ ///
+ public readonly RDPointI? Location => Left is null && Right is null ? null : new(Left, Bottom);
+
+ ///
+ /// Gets the size of the rectangle.
+ ///
+ public readonly RDSizeI? Size => Width is null && Height is null ? null : new(Width, Height);
+
+ ///
+ /// Inflates the rectangle by the specified size.
+ ///
+ /// The rectangle to inflate.
+ /// The size to inflate by.
+ /// The inflated rectangle.
+ public static RDRectI Inflate(RDRectI rect, RDSizeI size)
+ {
+ RDRectI result = new(rect.Left, rect.Top, rect.Right, rect.Bottom);
+ result.Inflate(size);
+ return result;
+ }
+
+ ///
+ /// Inflates the rectangle by the specified width and height.
+ ///
+ /// The rectangle to inflate.
+ /// The width to inflate by.
+ /// The height to inflate by.
+ /// The inflated rectangle.
+ public static RDRectI Inflate(RDRectI rect, int? x, int? y)
+ {
+ RDRectI result = new(rect.Left, rect.Top, rect.Right, rect.Bottom);
+ result.Inflate(x, y);
+ return result;
+ }
+
+ ///
+ /// Converts the specified RDRect to RDRectI using ceiling.
+ ///
+ /// The RDRect to convert.
+ /// The converted RDRectI.
+ public static RDRectI Ceiling(RDRect rect) => Ceiling(rect, false);
+
+ ///
+ /// Converts the specified RDRect to RDRectI using ceiling, and specifies whether to expand outwards.
+ ///
+ /// The RDRect to convert.
+ /// Whether to expand outwards.
+ /// The converted RDRectI.
+ public static RDRectI Ceiling(RDRect rect, bool outwards) => new(
+ rect.Left == null ? null : (int)(outwards && rect.Width > 0 ? Math.Floor((double)rect.Left) : Math.Ceiling((double)rect.Left)),
+ rect.Top == null ? null : (int)(outwards && rect.Height > 0 ? Math.Floor((double)rect.Top) : Math.Ceiling((double)rect.Top)),
+ rect.Right == null ? null : (int)(outwards && rect.Width < 0 ? Math.Floor((double)rect.Right) : Math.Ceiling((double)rect.Right)),
+ rect.Bottom == null ? null : (int)(outwards && rect.Height < 0 ? Math.Floor((double)rect.Bottom) : Math.Ceiling((double)rect.Bottom)));
+
+ ///
+ /// Converts the specified RDRect to RDRectI using floor.
+ ///
+ /// The RDRect to convert.
+ /// The converted RDRectI.
+ public static RDRectI Floor(RDRect rect) => Ceiling(rect, false);
+
+ ///
+ /// Converts the specified RDRect to RDRectI using floor, and specifies whether to shrink inwards.
+ ///
+ /// The RDRect to convert.
+ /// Whether to shrink inwards.
+ /// The converted RDRectI.
+ public static RDRectI Floor(RDRect rect, bool inwards) => new(
+ rect.Left == null ? null : (int)(inwards && rect.Width > 0 ? Math.Ceiling((double)rect.Left) : Math.Floor((double)rect.Left)),
+ rect.Top == null ? null : (int)(inwards && rect.Height > 0 ? Math.Ceiling((double)rect.Top) : Math.Floor((double)rect.Top)),
+ rect.Right == null ? null : (int)(inwards && rect.Width < 0 ? Math.Ceiling((double)rect.Right) : Math.Floor((double)rect.Right)),
+ rect.Bottom == null ? null : (int)(inwards && rect.Height < 0 ? Math.Ceiling((double)rect.Bottom) : Math.Floor((double)rect.Bottom)));
+
+ ///
+ /// Converts the specified RDRect to RDRectI using rounding.
+ ///
+ /// The RDRect to convert.
+ /// The converted RDRectI.
+ public static RDRectI Round(RDRect rect) => new(
+ new int?((int)Math.Round((rect.Left == null) ? 0.0 : Math.Round((double)rect.Left.Value))),
+ new int?((int)Math.Round((rect.Top == null) ? 0.0 : Math.Round((double)rect.Top.Value))),
+ new int?((int)Math.Round((rect.Right == null) ? 0.0 : Math.Round((double)rect.Right.Value))),
+ new int?((int)Math.Round((rect.Bottom == null) ? 0.0 : Math.Round((double)rect.Bottom.Value))));
+
+ ///
+ /// Returns a new RDRectI that is the union of two rectangles.
+ ///
+ /// The first rectangle.
+ /// The second rectangle.
+ /// The union of the two rectangles.
+ public static RDRectI Union(RDRectI rect1, RDRectI rect2) => new(
+ new int?((rect1.Left == null || rect2.Left == null) ? 0 : Math.Min(rect1.Left.Value, rect2.Left.Value)),
+ new int?((rect1.Top == null || rect2.Top == null) ? 0 : Math.Min(rect1.Top.Value, rect2.Top.Value)),
+ new int?((rect1.Right == null || rect2.Right == null) ? 0 : Math.Min(rect1.Right.Value, rect2.Right.Value)),
+ new int?((rect1.Bottom == null || rect2.Bottom == null) ? 0 : Math.Min(rect1.Bottom.Value, rect2.Bottom.Value)));
+
+ ///
+ /// Returns a new RDRectI that is the intersection of two rectangles.
+ ///
+ /// The first rectangle.
+ /// The second rectangle.
+ /// The intersection of the two rectangles.
+ public static RDRectI Intersect(RDRectI rect1, RDRectI rect2) => rect1.IntersectsWithInclusive(rect2) ? new RDRectI(
+ new int?((rect1.Left == null || rect2.Left == null) ? 0 : Math.Max(rect1.Left.Value, rect2.Left.Value)),
+ new int?((rect1.Top == null || rect2.Top == null) ? 0 : Math.Max(rect1.Top.Value, rect2.Top.Value)),
+ new int?((rect1.Right == null || rect2.Right == null) ? 0 : Math.Min(rect1.Right.Value, rect2.Right.Value)),
+ new int?((rect1.Bottom == null || rect2.Bottom == null) ? 0 : Math.Min(rect1.Bottom.Value, rect2.Bottom.Value))) : default;
+
+ ///
+ /// Converts the specified RDRect to RDRectI by truncating the decimal part.
+ ///
+ /// The RDRect to convert.
+ /// The converted RDRectI.
+ public static RDRectI Truncate(RDRect rect) => new(
+ (int?)rect.Left,
+ (int?)rect.Top,
+ (int?)rect.Right,
+ (int?)rect.Bottom);
+
+ ///
+ /// Offsets the rectangle by the specified amounts.
+ ///
+ /// The horizontal offset.
+ /// The vertical offset.
+ public void Offset(int? x, int? y)
+ {
+ Left += x;
+ Top += y;
+ Right += x;
+ Bottom += y;
+ }
+ ///
+ /// Moves the rectangle by the specified point.
+ ///
+ /// The point containing the offset.
+ public void Offset(RDPointI p) => Offset(p.X, p.Y);
+
+ ///
+ /// Inflates the rectangle by the specified size.
+ ///
+ /// The size to inflate by.
+ public void Inflate(RDSizeI size)
+ {
+ Left -= size.Width;
+ Top += size.Height;
+ Right += size.Width;
+ Bottom -= size.Height;
+ }
+
+ ///
+ /// Inflates the rectangle by the specified width and height.
+ ///
+ /// The width to inflate by.
+ /// The height to inflate by.
+ public void Inflate(int? width, int? height)
+ {
+ Left -= width;
+ Top += height;
+ Right += width;
+ Bottom -= height;
+ }
+ ///
+ /// Returns a new RDRectI that is the union of the current rectangle and the specified rectangle.
+ ///
+ /// The rectangle to merge with.
+ /// The union of the two rectangles.
+ public readonly RDRectI Union(RDRectI rect) => Union(this, rect);
+ ///
+ /// Determines whether the current rectangle intersects with the specified rectangle (including edges).
+ ///
+ /// The rectangle to check.
+ /// true if the two rectangles intersect (including edges); otherwise, false.
+ public readonly bool IntersectsWithInclusive(RDRectI rect) => Left <= rect.Right && Right >= rect.Left && Top <= rect.Bottom && Bottom >= rect.Top;
+ ///
+ public static bool operator ==(RDRectI rect1, RDRectI rect2) => rect1.Equals(rect2);
+ ///
+ public static bool operator !=(RDRectI rect1, RDRectI rect2) => !rect1.Equals(rect2);
+ ///
+ public override readonly bool Equals([NotNullWhen(true)] object? obj) => obj is RDRectI e && Equals(e);
+ ///
+ public override readonly int GetHashCode() => HashCode.Combine(Left, Top, Right, Bottom);
+ ///
+ public override readonly string ToString() => $"{{Location=[{Left},{Bottom}],Size=[{Width},{Height}]}}";
+ ///
+ public readonly bool Equals(RDRectI other) => Left == other.Left && Top == other.Top && Right == other.Right && Bottom == other.Bottom;
+
+ ///
+ /// Implicitly converts an to an .
+ ///
+ /// The instance to convert.
+ /// The converted instance.
+ public static implicit operator RDRect(RDRectI rect) => new(rect.Left, rect.Top, rect.Right, rect.Bottom);
+
+ ///
+ /// Implicitly converts an to an .
+ ///
+ /// The instance to convert.
+ /// The converted instance.
+ public static implicit operator RDRectE(RDRectI rect) => new(rect.Left, rect.Top, rect.Right, rect.Bottom);
+ private readonly string GetDebuggerDisplay() => ToString();
+ }
+}
diff --git a/RhythmBaseCore/Components/RDRectN.cs b/RhythmBaseCore/Components/RDRectN.cs
new file mode 100644
index 0000000..b11666b
--- /dev/null
+++ b/RhythmBaseCore/Components/RDRectN.cs
@@ -0,0 +1,254 @@
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+namespace RhythmBase.Components
+{
+ ///
+ /// Represents a rectangle defined by its left, top, right, and bottom edges.
+ ///
+ /// The left edge of the rectangle.
+ /// The top edge of the rectangle.
+ /// The right edge of the rectangle.
+ /// The bottom edge of the rectangle.
+ [DebuggerDisplay($"{{{nameof(GetDebuggerDisplay)}(),nq}}")]
+ public struct RDRectN(float left, float top, float right, float bottom)
+ {
+ ///
+ /// Gets or sets the left edge of the rectangle.
+ ///
+ public float Left { get; set; } = left;
+
+ ///
+ /// Gets or sets the right edge of the rectangle.
+ ///
+ public float Right { get; set; } = top;
+
+ ///
+ /// Gets or sets the top edge of the rectangle.
+ ///
+ public float Top { get; set; } = right;
+
+ ///
+ /// Gets or sets the bottom edge of the rectangle.
+ ///
+ public float Bottom { get; set; } = bottom;
+
+ ///
+ /// Gets the point at the left-bottom corner of the rectangle.
+ ///
+ public readonly RDPointN LeftBottom => new(Left, Bottom);
+
+ ///
+ /// Gets the point at the right-bottom corner of the rectangle.
+ ///
+ public readonly RDPointN RightBottom => new(Right, Bottom);
+
+ ///
+ /// Gets the point at the left-top corner of the rectangle.
+ ///
+ public readonly RDPointN LeftTop => new(Left, Top);
+
+ ///
+ /// Gets the point at the right-top corner of the rectangle.
+ ///
+ public readonly RDPointN RightTop => new(Right, Top);
+
+ ///
+ /// Gets the width of the rectangle.
+ ///
+ public readonly float Width => Right - Left;
+
+ ///
+ /// Gets the height of the rectangle.
+ ///
+ public readonly float Height => Top - Bottom;
+
+ ///
+ /// Initializes a new instance of the struct with the specified location and size.
+ ///
+ /// The location of the rectangle.
+ /// The size of the rectangle.
+ public RDRectN(RDPointN location, RDSizeN size) : this(location.X, location.Y + size.Height, location.X + size.Width, location.Y) { }
+
+ ///
+ /// Initializes a new instance of the struct with the specified size.
+ ///
+ /// The size of the rectangle.
+ public RDRectN(RDSizeN size) : this(0f, size.Height, size.Width, 0f) { }
+
+ ///
+ /// Initializes a new instance of the struct with the specified width and height.
+ ///
+ /// The width of the rectangle.
+ /// The height of the rectangle.
+ public RDRectN(float width, float height) : this(0f, height, width, 0f) { }
+
+ ///
+ /// Gets the location of the rectangle as an .
+ ///
+ public readonly RDPointNI Location => new((int)Math.Round((double)Left), (int)Math.Round((double)Bottom));
+
+ ///
+ /// Gets the size of the rectangle as an .
+ ///
+ public readonly RDSizeNI Size => (new RDSizeNI((int)Math.Round((double)Width), (int)Math.Round((double)Height)));
+
+ ///
+ /// Inflates the specified rectangle by the specified size.
+ ///
+ /// The rectangle to inflate.
+ /// The size to inflate by.
+ /// The inflated rectangle.
+ public static RDRectN Inflate(RDRectN rect, RDSizeNI size)
+ {
+ RDRectN result = new(rect.Left, rect.Top, rect.Right, rect.Bottom);
+ result.Inflate(size);
+ return result;
+ }
+
+ ///
+ /// Inflates the specified rectangle by the specified width and height.
+ ///
+ /// The rectangle to inflate.
+ /// The width to inflate by.
+ /// The height to inflate by.
+ /// The inflated rectangle.
+ public static RDRectN Inflate(RDRectN rect, float x, float y)
+ {
+ RDRectN result = new(rect.Left, rect.Top, rect.Right, rect.Bottom);
+ result.Inflate(x, y);
+ return result;
+ }
+
+ ///
+ /// Determines whether the rectangle contains the specified point.
+ ///
+ /// The x-coordinate of the point.
+ /// The y-coordinate of the point.
+ /// true if the rectangle contains the point; otherwise, false.
+ public readonly bool Contains(float x, float y) => Left < x && x < Right && Bottom < y && y < Top;
+
+ ///
+ /// Determines whether the rectangle contains the specified point.
+ ///
+ /// The point to check.
+ /// true if the rectangle contains the point; otherwise, false.
+ public readonly bool Contains(RDPointN p) => Left < p.X && p.X < Right && Bottom < p.Y && p.Y < Top;
+
+ ///
+ /// Determines whether the rectangle contains the specified rectangle.
+ ///
+ /// The rectangle to check.
+ /// true if the rectangle contains the specified rectangle; otherwise, false.
+ public readonly bool Contains(RDRectN rect) => Left < rect.Left && rect.Right < Right && Bottom < rect.Bottom && rect.Top < Top;
+
+ ///
+ /// Returns the union of two rectangles.
+ ///
+ /// The first rectangle.
+ /// The second rectangle.
+ /// The union of the two rectangles.
+ public static RDRectN Union(RDRectN rect1, RDRectN rect2) => new(Math.Min(rect1.Left, rect2.Left), Math.Max(rect1.Top, rect2.Top), Math.Max(rect1.Right, rect2.Right), Math.Min(rect1.Bottom, rect2.Bottom));
+
+ ///
+ /// Returns the intersection of two rectangles.
+ ///
+ /// The first rectangle.
+ /// The second rectangle.
+ /// The intersection of the two rectangles, or the default rectangle if they do not intersect.
+ public static RDRectN Intersect(RDRectN rect1, RDRectN rect2) => rect1.IntersectsWithInclusive(rect2) ? new RDRectN(
+ Math.Max(rect1.Left, rect2.Left),
+ Math.Max(rect1.Top, rect2.Top),
+ Math.Min(rect1.Right, rect2.Right),
+ Math.Min(rect1.Bottom, rect2.Bottom)) : default;
+
+ ///
+ /// Truncates the specified rectangle.
+ ///
+ /// The rectangle to truncate.
+ /// The truncated rectangle.
+ public static RDRectN Truncate(RDRectN rect) => new(rect.Left, rect.Top, rect.Right, rect.Bottom);
+
+ ///
+ /// Offsets the rectangle by the specified amount.
+ ///
+ /// The amount to offset by along the x-axis.
+ /// The amount to offset by along the y-axis.
+ public void Offset(float x, float y)
+ {
+ Left += x;
+ Top += y;
+ Right += x;
+ Bottom += y;
+ }
+
+ ///
+ /// Offsets the rectangle by the specified point.
+ ///
+ /// The point to offset by.
+ public void Offset(RDPointN p) => Offset(p.X, p.Y);
+
+ ///
+ /// Inflates the rectangle by the specified size.
+ ///
+ /// The size to inflate by.
+ public void Inflate(RDSizeN size)
+ {
+ Left -= size.Width;
+ Top += size.Height;
+ Right += size.Width;
+ Bottom -= size.Height;
+ }
+
+ ///
+ /// Inflates the rectangle by the specified width and height.
+ ///
+ /// The width to inflate by.
+ /// The height to inflate by.
+ public void Inflate(float width, float height)
+ {
+ Left -= width;
+ Top += height;
+ Right += width;
+ Bottom -= height;
+ }
+ ///
+ /// Returns the union of this rectangle with the specified rectangle.
+ ///
+ /// The rectangle to union with.
+ /// The union of the two rectangles.
+ public readonly RDRectN Union(RDRectN rect) => Union(this, rect);
+ ///
+ /// Determines whether this rectangle intersects with the specified rectangle, including the edges.
+ ///
+ /// The rectangle to check for intersection.
+ /// true if the rectangles intersect; otherwise, false.
+ public readonly bool IntersectsWithInclusive(RDRectN rect) => Left <= rect.Right && Right >= rect.Left && Top <= rect.Bottom && Bottom >= rect.Top;
+ ///
+ public static bool operator ==(RDRectN rect1, RDRectN rect2) => rect1.Equals(rect2);
+ ///
+ public static bool operator !=(RDRectN rect1, RDRectN rect2) => !rect1.Equals(rect2);
+ ///
+ public override readonly bool Equals([NotNullWhen(true)] object? obj) => obj is RDRectN e && Equals(e);
+ ///
+ public override readonly int GetHashCode() => HashCode.Combine(Left, Top, Right, Bottom);
+ ///
+ public override readonly string ToString() => $"{{Location=[{Left},{Bottom}],Size=[{Width},{Height}]}}";
+ ///
+ public readonly bool Equals(RDRectN other) => Left == other.Left && Top == other.Top && Right == other.Right && Bottom == other.Bottom;
+
+ ///
+ /// Converts an to an .
+ ///
+ /// The to convert.
+ /// An that represents the same rectangle.
+ public static implicit operator RDRect(RDRectN rect) => new(rect.Left, rect.Top, rect.Right, rect.Bottom);
+
+ ///
+ /// Converts an to an .
+ ///
+ /// The to convert.
+ /// An that represents the same rectangle.
+ public static implicit operator RDRectE(RDRectN rect) => new(rect.Left, rect.Top, rect.Right, rect.Bottom);
+ private readonly string GetDebuggerDisplay() => ToString();
+ }
+}
diff --git a/RhythmBaseCore/Components/RDRectNI.cs b/RhythmBaseCore/Components/RDRectNI.cs
new file mode 100644
index 0000000..bb27474
--- /dev/null
+++ b/RhythmBaseCore/Components/RDRectNI.cs
@@ -0,0 +1,333 @@
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+namespace RhythmBase.Components
+{
+ ///
+ /// Represents a rectangle defined by its left, top, right, and bottom coordinates.
+ ///
+ /// The left coordinate of the rectangle.
+ /// The top coordinate of the rectangle.
+ /// The right coordinate of the rectangle.
+ /// The bottom coordinate of the rectangle.
+ [DebuggerDisplay($"{{{nameof(GetDebuggerDisplay)}(),nq}}")]
+ public struct RDRectNI(int left, int top, int right, int bottom) : IEquatable
+ {
+ ///
+ /// Gets or sets the left coordinate of the rectangle.
+ ///
+ public int Left { get; set; } = left;
+
+ ///
+ /// Gets or sets the right coordinate of the rectangle.
+ ///
+ public int Right { get; set; } = right;
+
+ ///
+ /// Gets or sets the top coordinate of the rectangle.
+ ///
+ public int Top { get; set; } = top;
+
+ ///
+ /// Gets or sets the bottom coordinate of the rectangle.
+ ///
+ public int Bottom { get; set; } = bottom;
+
+ ///
+ /// Gets the bottom-left point of the rectangle.
+ ///
+ public readonly RDPointNI LeftBottom => new(Left, Bottom);
+
+ ///
+ /// Gets the bottom-right point of the rectangle.
+ ///
+ public readonly RDPointNI RightBottom => new(Right, Bottom);
+
+ ///
+ /// Gets the top-left point of the rectangle.
+ ///
+ public readonly RDPointNI LeftTop => new(Left, Top);
+
+ ///
+ /// Gets the top-right point of the rectangle.
+ ///
+ public readonly RDPointNI RightTop => new(Right, Top);
+
+ ///
+ /// Gets the width of the rectangle.
+ ///
+ public readonly int Width => Right - Left;
+
+ ///
+ /// Gets the height of the rectangle.
+ ///
+ public readonly int Height => Top - Bottom;
+
+ ///
+ /// Initializes a new instance of the struct with the specified location and size.
+ ///
+ /// The location of the rectangle.
+ /// The size of the rectangle.
+ public RDRectNI(RDPointNI location, RDSizeNI size) : this(location.X, location.Y + size.Height, location.X + size.Width, location.Y) { }
+
+ ///
+ /// Initializes a new instance of the struct with the specified size.
+ ///
+ /// The size of the rectangle.
+ public RDRectNI(RDSizeNI size) : this(0, size.Height, size.Width, 0) { }
+
+ ///
+ /// Initializes a new instance of the struct with the specified width and height.
+ ///
+ /// The width of the rectangle.
+ /// The height of the rectangle.
+ public RDRectNI(int width, int height) : this(0, height, width, 0) { }
+
+ ///
+ /// Gets the location of the rectangle.
+ ///
+ public readonly RDPointNI Location => new(Left, Bottom);
+
+ ///
+ /// Gets the size of the rectangle.
+ ///
+ public readonly RDSizeNI Size => new(Width, Height);
+
+ ///
+ /// Inflates the specified rectangle by the specified size.
+ ///
+ /// The rectangle to inflate.
+ /// The size to inflate by.
+ /// The inflated rectangle.
+ public static RDRectNI Inflate(RDRectNI rect, RDSizeNI size)
+ {
+ RDRectNI result = new(rect.Left, rect.Top, rect.Right, rect.Bottom);
+ result.Inflate(size);
+ return result;
+ }
+
+ ///
+ /// Inflates the specified rectangle by the specified amounts.
+ ///
+ /// The rectangle to inflate.
+ /// The amount to inflate the width by.
+ /// The amount to inflate the height by.
+ /// The inflated rectangle.
+ public static RDRectNI Inflate(RDRectNI rect, int x, int y)
+ {
+ RDRectNI result = new(rect.Left, rect.Top, rect.Right, rect.Bottom);
+ result.Inflate(x, y);
+ return result;
+ }
+ ///
+ /// Returns a rectangle structure that represents the smallest possible rectangle that can contain the specified rectangle, with each value rounded up to the nearest integer.
+ ///
+ /// The rectangle to be rounded up.
+ /// A new rectangle structure with each value rounded up to the nearest integer.
+ public static RDRectNI Ceiling(RDRectN rect) => Ceiling(rect, false);
+
+ ///
+ /// Returns a rectangle structure that represents the smallest possible rectangle that can contain the specified rectangle, with each value rounded up or down to the nearest integer.
+ ///
+ /// The rectangle to be rounded.
+ /// If true, values are rounded outwards; otherwise, they are rounded inwards.
+ /// A new rectangle structure with each value rounded to the nearest integer.
+ public static RDRectNI Ceiling(RDRectN rect, bool outwards) => new(
+ (int)Math.Round((outwards && rect.Width > 0f) ? Math.Floor((double)rect.Left) : Math.Ceiling((double)rect.Left)),
+ (int)Math.Round((outwards && rect.Height > 0f) ? Math.Floor((double)rect.Top) : Math.Ceiling((double)rect.Top)),
+ (int)Math.Round((outwards && rect.Width < 0f) ? Math.Floor((double)rect.Right) : Math.Ceiling((double)rect.Right)),
+ (int)Math.Round((outwards && rect.Height < 0f) ? Math.Floor((double)rect.Bottom) : Math.Ceiling((double)rect.Bottom)));
+
+ ///
+ /// Returns a rectangle structure that represents the largest possible rectangle that can fit within the specified rectangle, with each value rounded down to the nearest integer.
+ ///
+ /// The rectangle to be rounded down.
+ /// A new rectangle structure with each value rounded down to the nearest integer.
+ public static RDRectNI Floor(RDRectN rect) => Ceiling(rect, false);
+
+ ///
+ /// Returns a rectangle structure that represents the largest possible rectangle that can fit within the specified rectangle, with each value rounded up or down to the nearest integer.
+ ///
+ /// The rectangle to be rounded.
+ /// If true, values are rounded inwards; otherwise, they are rounded outwards.
+ /// A new rectangle structure with each value rounded to the nearest integer.
+ public static RDRectNI Floor(RDRectN rect, bool inwards) => new(
+ (int)Math.Round((inwards && rect.Width > 0f) ? Math.Ceiling((double)rect.Left) : Math.Floor((double)rect.Left)),
+ (int)Math.Round((inwards && rect.Height > 0f) ? Math.Ceiling((double)rect.Top) : Math.Floor((double)rect.Top)),
+ (int)Math.Round((inwards && rect.Width < 0f) ? Math.Ceiling((double)rect.Right) : Math.Floor((double)rect.Right)),
+ (int)Math.Round((inwards && rect.Height < 0f) ? Math.Ceiling((double)rect.Bottom) : Math.Floor((double)rect.Bottom)));
+
+ ///
+ /// Returns a rectangle structure that represents the specified rectangle, with each value rounded to the nearest integer.
+ ///
+ /// The rectangle to be rounded.
+ /// A new rectangle structure with each value rounded to the nearest integer.
+ public static RDRectNI Round(RDRectN rect) => new(
+ (int)Math.Round((double)rect.Left),
+ (int)Math.Round((double)rect.Top),
+ (int)Math.Round((double)rect.Right),
+ (int)Math.Round((double)rect.Bottom));
+
+ ///
+ /// Returns a rectangle structure that represents the union of two rectangles. The union is the smallest rectangle that contains both rectangles.
+ ///
+ /// The first rectangle.
+ /// The second rectangle.
+ /// A new rectangle structure that represents the union of the two rectangles.
+ public static RDRectNI Union(RDRectNI rect1, RDRectNI rect2) => new(
+ Math.Min(rect1.Left, rect2.Left),
+ Math.Max(rect1.Top, rect2.Top),
+ Math.Max(rect1.Right, rect2.Right),
+ Math.Min(rect1.Bottom, rect2.Bottom));
+
+ ///
+ /// Returns a rectangle structure that represents the intersection of two rectangles. The intersection is the largest rectangle that is contained within both rectangles.
+ ///
+ /// The first rectangle.
+ /// The second rectangle.
+ /// A new rectangle structure that represents the intersection of the two rectangles, or the default rectangle if there is no intersection.
+ public static RDRectNI Intersect(RDRectNI rect1, RDRectNI rect2) => rect1.IntersectsWithInclusive(rect2) ? new RDRectNI(
+ Math.Max(rect1.Left, rect2.Left),
+ Math.Max(rect1.Top, rect2.Top),
+ Math.Min(rect1.Right, rect2.Right),
+ Math.Min(rect1.Bottom, rect2.Bottom)) : default;
+
+ ///
+ /// Returns a rectangle structure that represents the specified rectangle, with each value truncated to the nearest integer.
+ ///
+ /// The rectangle to be truncated.
+ /// A new rectangle structure with each value truncated to the nearest integer.
+ public static RDRectNI Truncate(RDRectN rect) => new(
+ (int)Math.Round((double)rect.Left),
+ (int)Math.Round((double)rect.Top),
+ (int)Math.Round((double)rect.Right),
+ (int)Math.Round((double)rect.Bottom));
+
+ ///
+ /// Moves the rectangle by the specified horizontal and vertical amounts.
+ ///
+ /// The amount to move the rectangle horizontally.
+ /// The amount to move the rectangle vertically.
+ public void Offset(int x, int y)
+ {
+ Left += x;
+ Top += y;
+ Right += x;
+ Bottom += y;
+ }
+
+ ///
+ /// Moves the rectangle by the specified point.
+ ///
+ /// The point to move the rectangle by.
+ public void Offset(RDPointNI p) => Offset(p.X, p.Y);
+
+ ///
+ /// Inflates the rectangle by the specified size.
+ ///
+ /// The size to inflate the rectangle by.
+ public void Inflate(RDSizeNI size)
+ {
+ Left -= size.Width;
+ Top += size.Height;
+ Right += size.Width;
+ Bottom -= size.Height;
+ }
+
+ ///
+ /// Inflates the rectangle by the specified width and height.
+ ///
+ /// The amount to inflate the rectangle's width by.
+ /// The amount to inflate the rectangle's height by.
+ public void Inflate(int width, int height)
+ {
+ Left -= width;
+ Top += height;
+ Right += width;
+ Bottom -= height;
+ }
+
+ ///
+ /// Determines whether the rectangle contains the specified point.
+ ///
+ /// The x-coordinate of the point.
+ /// The y-coordinate of the point.
+ /// True if the rectangle contains the point; otherwise, false.
+ public readonly bool Contains(int x, int y) => Left < x && x < Right && Bottom < y && y < Top;
+
+ ///
+ /// Determines whether the rectangle contains the specified point.
+ ///
+ /// The point to check.
+ /// True if the rectangle contains the point; otherwise, false.
+ public readonly bool Contains(RDPointN p) => (float)Left < p.X && p.X < (float)Right && (float)Bottom < p.Y && p.Y < (float)Top;
+
+ ///
+ /// Determines whether the rectangle contains the specified rectangle.
+ ///
+ /// The rectangle to check.
+ /// True if the rectangle contains the specified rectangle; otherwise, false.
+ public readonly bool Contains(RDRectNI rect) => Left < rect.Left && rect.Right < Right && Bottom < rect.Bottom && rect.Top < Top;
+
+ ///
+ /// Returns a rectangle structure that represents the union of this rectangle and the specified rectangle.
+ ///
+ /// The rectangle to union with.
+ /// A new rectangle structure that represents the union of the two rectangles.
+ public readonly RDRectNI Union(RDRectNI rect) => Union(this, rect);
+
+ ///
+ /// Returns a rectangle structure that represents the intersection of this rectangle and the specified rectangle.
+ ///
+ /// The rectangle to intersect with.
+ /// A new rectangle structure that represents the intersection of the two rectangles, or the default rectangle if there is no intersection.
+ public readonly object Intersect(RDRectNI rect) => Intersect(this, rect);
+
+ ///
+ /// Determines whether this rectangle intersects with the specified rectangle.
+ ///
+ /// The rectangle to check for intersection.
+ /// True if the rectangles intersect; otherwise, false.
+ public readonly bool IntersectsWith(RDRectNI rect) => Left < rect.Right && Right > rect.Left && Top < rect.Bottom && Bottom > rect.Top;
+
+ ///
+ /// Determines whether this rectangle intersects with the specified rectangle, including the edges.
+ ///
+ /// The rectangle to check for intersection.
+ /// True if the rectangles intersect, including the edges; otherwise, false.
+ public readonly bool IntersectsWithInclusive(RDRectNI rect) => Left <= rect.Right && Right >= rect.Left && Top <= rect.Bottom && Bottom >= rect.Top;
+ ///
+ public static bool operator ==(RDRectNI rect1, RDRectNI rect2) => rect1.Equals(rect2);
+ ///
+ public static bool operator !=(RDRectNI rect1, RDRectNI rect2) => !rect1.Equals(rect2);
+ ///
+ public override readonly bool Equals([NotNullWhen(true)] object? obj) => obj is RDRectNI e && Equals(e);
+ ///
+ public override readonly int GetHashCode() => HashCode.Combine(Left, Top, Right, Bottom);
+ ///
+ public override readonly string ToString() => $"{{Location=[{Left},{Bottom}],Size=[{Width},{Height}]}}";
+ ///
+ public readonly bool Equals(RDRectNI other) => Left == other.Left && Top == other.Top && Right == other.Right && Bottom == other.Bottom;
+
+ ///
+ /// Implicitly converts an to an .
+ ///
+ /// The to convert.
+ /// An that represents the same rectangle.
+ public static implicit operator RDRectN(RDRectNI rect) => new(rect.Left, rect.Top, rect.Right, rect.Bottom);
+
+ ///
+ /// Implicitly converts an to an .
+ ///
+ /// The to convert.
+ /// An that represents the same rectangle.
+ public static implicit operator RDRectI(RDRectNI rect) => new(rect.Left, rect.Top, rect.Right, rect.Bottom);
+
+ ///
+ /// Implicitly converts an to an .
+ ///
+ /// The to convert.
+ /// An that represents the same rectangle.
+ public static implicit operator RDRectE(RDRectNI rect) => new(rect.Left, rect.Top, rect.Right, rect.Bottom);
+ private readonly string GetDebuggerDisplay() => ToString();
+ }
+}
diff --git a/RhythmBaseCore/Components/RDRoom.cs b/RhythmBaseCore/Components/RDRoom.cs
new file mode 100644
index 0000000..7e5183e
--- /dev/null
+++ b/RhythmBaseCore/Components/RDRoom.cs
@@ -0,0 +1,155 @@
+using Microsoft.VisualBasic.CompilerServices;
+using Newtonsoft.Json;
+using RhythmBase.Converters;
+using System.Numerics;
+using System.Runtime.CompilerServices;
+namespace RhythmBase.Components
+{
+ ///
+ /// Represents a room that can be applied to multiple rooms.
+ ///
+ [JsonConverter(typeof(RoomConverter))]
+ public struct RDRoom : IEqualityOperators, IEquatable
+ {
+ ///
+ /// Indicates if the top room can be applied.
+ ///
+ public bool EnableTop { get; }
+
+ ///
+ /// Gets or sets whether the specified room is enabled.
+ ///
+ /// The index of the room.
+ /// True if the room is enabled; otherwise, false.
+ [IndexerName("Room")]
+ public bool this[byte Index]
+ {
+ readonly get => _data.HasFlag((RDRoomIndex)(1 << Index));
+ set
+ {
+ if (!(Index >= 4 && !EnableTop))
+ _data = value ? (_data | (RDRoomIndex)(1 << (int)Index)) : (_data & (RDRoomIndex)(1 << (int)Index));
+ }
+ }
+
+ ///
+ /// Gets the list of enabled rooms.
+ ///
+ public readonly List Rooms
+ {
+ get
+ {
+ RDRoomIndex indexes = _data;
+ return Enumerable
+ .Range(0, 5)
+ .Where(x => indexes.HasFlag((RDRoomIndex)(1 << x)))
+ .Select(x => (byte)x)
+ .ToList();
+ }
+ }
+
+ ///
+ public override readonly string ToString() => string.Format("[{0}]", string.Join(",", Rooms));
+
+ ///
+ /// Returns an instance with only room 1 enabled.
+ ///
+ /// An instance with only room 1 enabled.
+ public static RDRoom Default() => new(false, [])
+ {
+ _data = RDRoomIndex.Room1
+ };
+
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ /// Indicates if the top room can be applied.
+ public RDRoom(bool enableTop) => EnableTop = enableTop;
+
+ ///
+ /// Initializes a new instance of the struct with specified rooms.
+ ///
+ /// Indicates if the top room can be applied.
+ /// The rooms to be enabled.
+ public RDRoom(bool enableTop, params byte[] rooms)
+ {
+ this = default;
+ EnableTop = enableTop;
+ int num = rooms.Length;
+ if (num != 0)
+ if (num != 1)
+ foreach (byte item in rooms)
+ this[item] = true;
+ else
+ this[rooms.Single()] = true;
+ else
+ _data = RDRoomIndex.RoomNotAvaliable;
+ }
+
+ ///
+ /// Checks if the specified rooms are included.
+ ///
+ /// The rooms to check.
+ /// True if the rooms are included; otherwise, false.
+ public readonly bool Contains(RDRoom rooms)
+ {
+ if (_data == RDRoomIndex.RoomNotAvaliable)
+ return false;
+ else
+ {
+ for (int i = 0; i < 5; i++)
+ {
+ if (this[(byte)i] != rooms[(byte)i])
+ break;
+ if (i > 4)
+ return true;
+ }
+ return false;
+ }
+ }
+ ///
+ /// Checks if the specified room is included.
+ ///
+ /// The room to check.
+ /// True if the room is included; otherwise, false.
+ public readonly bool Contains(RDRoomIndex room)
+ {
+ return _data.HasFlag(room);
+ }
+
+ ///
+ public static bool operator ==(RDRoom R1, RDRoom R2) => R1._data == R2._data;
+
+ ///
+ public static bool operator !=(RDRoom R1, RDRoom R2) => !(R1 == R2);
+
+ ///
+ /// Implicitly converts a SingleRoom to a Room.
+ ///
+ /// The SingleRoom instance to convert.
+ /// A Room instance.
+ public static implicit operator RDRoom(RDSingleRoom room) => new(room.EnableTop, [0, ((byte)room.Room)]);
+
+ ///
+ /// Explicitly converts a Room to a SingleRoom.
+ ///
+ /// The Room instance to convert.
+ /// A SingleRoom instance.
+ /// Thrown when the Room contains more than one room.
+ public static explicit operator RDSingleRoom(RDRoom room) =>
+ room.Rooms.Count == 1
+ ? new RDSingleRoom(room.Rooms.Single())
+ : throw new Exceptions.RhythmBaseException();
+
+ ///
+ public override readonly bool Equals(object? obj) => obj is RDRoom e && Equals(e);
+
+ ///
+ public override readonly int GetHashCode() => HashCode.Combine(_data);
+
+ ///
+ public readonly bool Equals(RDRoom other) => this == other;
+
+ private RDRoomIndex _data;
+ }
+}
diff --git a/RhythmBaseCore/Components/RDRoomIndex.cs b/RhythmBaseCore/Components/RDRoomIndex.cs
new file mode 100644
index 0000000..ef19441
--- /dev/null
+++ b/RhythmBaseCore/Components/RDRoomIndex.cs
@@ -0,0 +1,44 @@
+namespace RhythmBase.Components
+{
+ ///
+ /// Represents the index of a room with various possible values.
+ ///
+ [Flags]
+ public enum RDRoomIndex : byte
+ {
+ ///
+ /// No room selected.
+ ///
+ None = 0b0000_0000,
+
+ ///
+ /// Represents Room 1.
+ ///
+ Room1 = 0b0000_0001,
+
+ ///
+ /// Represents Room 2.
+ ///
+ Room2 = 0b0000_0010,
+
+ ///
+ /// Represents Room 3.
+ ///
+ Room3 = 0b0000_0100,
+
+ ///
+ /// Represents Room 4.
+ ///
+ Room4 = 0b0000_1000,
+
+ ///
+ /// Represents the top room.
+ ///
+ RoomTop = 0b0001_0000,
+
+ ///
+ /// Indicates that the room is not available.
+ ///
+ RoomNotAvaliable = byte.MaxValue,
+ }
+}
diff --git a/RhythmBaseCore/Components/RDRotatedRect.cs b/RhythmBaseCore/Components/RDRotatedRect.cs
new file mode 100644
index 0000000..9e430f2
--- /dev/null
+++ b/RhythmBaseCore/Components/RDRotatedRect.cs
@@ -0,0 +1,128 @@
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+namespace RhythmBase.Components
+{
+ ///
+ /// Represents a rotated rectangle with non-integer coordinates.
+ ///
+ [DebuggerDisplay($"{{{nameof(GetDebuggerDisplay)}(),nq}}")]
+ public struct RDRotatedRect(RDPoint location, RDSize size, RDPoint? pivot, float? angle = 0) : IEquatable
+ {
+ ///
+ /// Gets or sets the location of the rectangle.
+ ///
+ public RDPoint? Location { get; set; } = location;
+
+ ///
+ /// Gets or sets the size of the rectangle.
+ ///
+ public RDSize? Size { get; set; } = size;
+
+ ///
+ /// Gets or sets the pivot point of the rotation.
+ ///
+ public RDPoint? Pivot { get; set; } = pivot;
+
+ ///
+ /// Gets or sets the angle of rotation in degrees.
+ ///
+ public float? Angle { get; set; } = angle;
+
+ ///
+ /// Gets the rectangle without rotation.
+ ///
+ public readonly RDRect WithoutRotate => new(Location - (RDSize?)Pivot, Size);
+
+ ///
+ /// IItializes a new instance of the struct.
+ ///
+ /// The rectangle.
+ /// The pivot point.
+ /// The angle of rotation.
+ public RDRotatedRect(RDRect rect, RDPoint? pivot, float angle) : this(rect.Location, rect.Size, pivot, angle) { }
+
+ ///
+ /// IItializes a new instance of the struct.
+ ///
+ /// The rectangle.
+ public RDRotatedRect(RDRect rect) : this(rect.Location, rect.Size, default, 0f) { }
+
+ ///
+ /// Inflates the specified rectangle by the specified size.
+ ///
+ /// The rectangle to inflate.
+ /// The size to inflate by.
+ /// The inflated rectangle.
+ public static RDRotatedRect Inflate(RDRotatedRect rect, RDSize size)
+ {
+ RDRotatedRect result = rect;
+ result.Inflate(size);
+ return result;
+ }
+
+ ///
+ /// Inflates the specified rectangle by the specified width and height.
+ ///
+ /// The rectangle to inflate.
+ /// The width to inflate by.
+ /// The height to inflate by.
+ /// The inflated rectangle.
+ public static RDRotatedRect Inflate(RDRotatedRect rect, int x, int y)
+ {
+ RDRotatedRect result = rect;
+ result.Inflate(x, y);
+ return result;
+ }
+
+ ///
+ /// Offsets the rectangle by the specified x and y values.
+ ///
+ /// The x value to offset by.
+ /// The y value to offset by.
+ public void Offset(float? x, float? y) => Location += new RDSize(x, y);
+
+ ///
+ /// Offsets the rectangle by the specified point.
+ ///
+ /// The point to offset by.
+ public void Offset(RDPoint p) => Offset(p.X, p.Y);
+
+ ///
+ /// Inflates the rectangle by the specified size.
+ ///
+ /// The size to inflate by.
+ public void Inflate(RDSize size)
+ {
+ Size += new RDSize(size.Width * 2, size.Height * 2);
+ Pivot -= new RDSize(size.Width, size.Height);
+ }
+
+ ///
+ /// Inflates the rectangle by the specified width and height.
+ ///
+ /// The width to inflate by.
+ /// The height to inflate by.
+ public void Inflate(float? width, float? height)
+ {
+ Size += new RDSize(width * 2, height * 2);
+ Pivot -= new RDSize(width, height);
+ }
+ ///
+ public static bool operator ==(RDRotatedRect rect1, RDRotatedRect rect2) => rect1.Equals(rect2);
+ ///
+ public static bool operator !=(RDRotatedRect rect1, RDRotatedRect rect2) => !rect1.Equals(rect2);
+ ///
+ public override readonly bool Equals([NotNullWhen(true)] object? obj) => obj is RDRotatedRect e && Equals(e);
+ ///
+ public override readonly int GetHashCode() => HashCode.Combine(Location, Size, Pivot, Angle);
+ ///
+ public override readonly string ToString() => $"{{Location=[{Location}],Size=[{Size}],Pivot[{Pivot}],Angle={Angle}}}";
+ ///
+ public readonly bool Equals(RDRotatedRect other) =>
+ Location == other.Location &&
+ Size == other.Size && Pivot
+ == other.Pivot &&
+ Angle == other.Angle;
+ private readonly string GetDebuggerDisplay() => ToString();
+ }
+}
\ No newline at end of file
diff --git a/RhythmBaseCore/Components/RDRotatedRectE.cs b/RhythmBaseCore/Components/RDRotatedRectE.cs
new file mode 100644
index 0000000..6d8b037
--- /dev/null
+++ b/RhythmBaseCore/Components/RDRotatedRectE.cs
@@ -0,0 +1,128 @@
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+namespace RhythmBase.Components
+{
+ ///
+ /// Represents a rotated rectangle with non-integer coordinates.
+ ///
+ [DebuggerDisplay($"{{{nameof(GetDebuggerDisplay)}(),nq}}")]
+ public struct RDRotatedRectE(RDPointE? location, RDSizeE? size, RDPointE? pivot, RDExpression? angle = null) : IEquatable
+ {
+ ///
+ /// Gets or sets the location of the rectangle.
+ ///
+ public RDPointE? Location { get; set; } = location;
+
+ ///
+ /// Gets or sets the size of the rectangle.
+ ///
+ public RDSizeE? Size { get; set; } = size;
+
+ ///
+ /// Gets or sets the pivot point of the rotation.
+ ///
+ public RDPointE? Pivot { get; set; } = pivot;
+
+ ///
+ /// Gets or sets the angle of rotation in degrees.
+ ///
+ public RDExpression? Angle { get; set; } = angle;
+
+ ///
+ /// Gets the rectangle without rotation.
+ ///
+ public readonly RDRectE? WithoutRotate => Location is null && Pivot is null && Size is null ? null : new(Location - (RDSizeE)(Pivot??default), Size);
+
+ ///
+ /// IItializes a new instance of the struct.
+ ///
+ /// The rectangle.
+ /// The pivot point.
+ /// The angle of rotation.
+ public RDRotatedRectE(RDRectE? rect, RDPointE? pivot, float angle) : this(rect?.Location, rect?.Size, pivot, angle) { }
+
+ ///
+ /// IItializes a new instance of the struct.
+ ///
+ /// The rectangle.
+ public RDRotatedRectE(RDRectE? rect) : this(rect?.Location, rect?.Size, null, 0f) { }
+
+ ///
+ /// Inflates the specified rectangle by the specified size.
+ ///
+ /// The rectangle to inflate.
+ /// The size to inflate by.
+ /// The inflated rectangle.
+ public static RDRotatedRectE Inflate(RDRotatedRectE rect, RDSizeE size)
+ {
+ RDRotatedRectE result = rect;
+ result.Inflate(size);
+ return result;
+ }
+
+ ///
+ /// Inflates the specified rectangle by the specified width and height.
+ ///
+ /// The rectangle to inflate.
+ /// The width to inflate by.
+ /// The height to inflate by.
+ /// The inflated rectangle.
+ public static RDRotatedRectE Inflate(RDRotatedRectE rect, int x, int y)
+ {
+ RDRotatedRectE result = rect;
+ result.Inflate(x, y);
+ return result;
+ }
+
+ ///
+ /// Offsets the rectangle by the specified x and y values.
+ ///
+ /// The x value to offset by.
+ /// The y value to offset by.
+ public void Offset(RDExpression? x, RDExpression? y) => Location += (RDSizeE)new RDPointE(x, y);
+
+ ///
+ /// Offsets the rectangle by the specified point.
+ ///
+ /// The point to offset by.
+ public void Offset(RDPointE p) => Offset(p.X, p.Y);
+
+ ///
+ /// Inflates the rectangle by the specified size.
+ ///
+ /// The size to inflate by.
+ public void Inflate(RDSizeE size)
+ {
+ Size += new RDSizeE(size.Width * 2, size.Height * 2);
+ Pivot -= (RDSizeE)new RDPointE(size.Width, size.Height);
+ }
+
+ ///
+ /// Inflates the rectangle by the specified width and height.
+ ///
+ /// The width to inflate by.
+ /// The height to inflate by.
+ public void Inflate(int width, int height)
+ {
+ Size += new RDSizeE(width * 2, height * 2);
+ Pivot -= (RDSizeE)new RDPointE(width, height);
+ }
+ ///
+ public static bool operator ==(RDRotatedRectE rect1, RDRotatedRectE rect2) => rect1.Equals(rect2);
+ ///
+ public static bool operator !=(RDRotatedRectE rect1, RDRotatedRectE rect2) => !rect1.Equals(rect2);
+ ///
+ public override readonly bool Equals([NotNullWhen(true)] object? obj) => obj is RDRotatedRectE e && Equals(e);
+ ///
+ public override readonly int GetHashCode() => HashCode.Combine(Location, Size, Pivot, Angle);
+ ///
+ public override readonly string ToString() => $"{{Location=[{Location}],Size=[{Size}],Pivot[{Pivot}],Angle={Angle}}}";
+ ///
+ public readonly bool Equals(RDRotatedRectE other) =>
+ Location == other.Location &&
+ Size == other.Size && Pivot
+ == other.Pivot &&
+ Angle == other.Angle;
+ private readonly string GetDebuggerDisplay() => ToString();
+ }
+}
\ No newline at end of file
diff --git a/RhythmBaseCore/Components/RDRotatedRectI.cs b/RhythmBaseCore/Components/RDRotatedRectI.cs
new file mode 100644
index 0000000..dd1c52a
--- /dev/null
+++ b/RhythmBaseCore/Components/RDRotatedRectI.cs
@@ -0,0 +1,128 @@
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+namespace RhythmBase.Components
+{
+ ///
+ /// Represents a rotated rectangle with non-integer coordinates.
+ ///
+ [DebuggerDisplay($"{{{nameof(GetDebuggerDisplay)}(),nq}}")]
+ public struct RDRotatedRectI(RDPointI? location, RDSizeI? size, RDPointI? pivot, float? angle = 0) : IEquatable
+ {
+ ///
+ /// Gets or sets the location of the rectangle.
+ ///
+ public RDPointI? Location { get; set; } = location;
+
+ ///
+ /// Gets or sets the size of the rectangle.
+ ///
+ public RDSizeI? Size { get; set; } = size;
+
+ ///
+ /// Gets or sets the pivot point of the rotation.
+ ///
+ public RDPointI? Pivot { get; set; } = pivot;
+
+ ///
+ /// Gets or sets the angle of rotation in degrees.
+ ///
+ public float? Angle { get; set; } = angle;
+
+ ///
+ /// Gets the rectangle without rotation.
+ ///
+ public readonly RDRectI? WithoutRotate => Location is null && Pivot is null && Size is null ? null : new(Location - (RDSizeI?)Pivot, Size);
+
+ ///
+ /// IItializes a new instance of the struct.
+ ///
+ /// The rectangle.
+ /// The pivot point.
+ /// The angle of rotation.
+ public RDRotatedRectI(RDRectI? rect, RDPointI? pivot, float angle) : this(rect?.Location, rect?.Size, pivot, angle) { }
+
+ ///
+ /// IItializes a new instance of the struct.
+ ///
+ /// The rectangle.
+ public RDRotatedRectI(RDRectI? rect) : this(rect?.Location, rect?.Size, null, 0f) { }
+
+ ///
+ /// Inflates the specified rectangle by the specified size.
+ ///
+ /// The rectangle to inflate.
+ /// The size to inflate by.
+ /// The inflated rectangle.
+ public static RDRotatedRectI Inflate(RDRotatedRectI rect, RDSizeI size)
+ {
+ RDRotatedRectI result = rect;
+ result.Inflate(size);
+ return result;
+ }
+
+ ///
+ /// Inflates the specified rectangle by the specified width and height.
+ ///
+ /// The rectangle to inflate.
+ /// The width to inflate by.
+ /// The height to inflate by.
+ /// The inflated rectangle.
+ public static RDRotatedRectI Inflate(RDRotatedRectI rect, int x, int y)
+ {
+ RDRotatedRectI result = rect;
+ result.Inflate(x, y);
+ return result;
+ }
+
+ ///
+ /// Offsets the rectangle by the specified x and y values.
+ ///
+ /// The x value to offset by.
+ /// The y value to offset by.
+ public void Offset(int? x, int? y) => Location += (RDSizeI)new RDPointI(x, y);
+
+ ///
+ /// Offsets the rectangle by the specified point.
+ ///
+ /// The point to offset by.
+ public void Offset(RDPointI p) => Offset(p.X, p.Y);
+
+ ///
+ /// Inflates the rectangle by the specified size.
+ ///
+ /// The size to inflate by.
+ public void Inflate(RDSizeI size)
+ {
+ Size += new RDSizeI(size.Width * 2, size.Height * 2);
+ Pivot -= (RDSizeI)new RDPointI(size.Width, size.Height);
+ }
+
+ ///
+ /// Inflates the rectangle by the specified width and height.
+ ///
+ /// The width to inflate by.
+ /// The height to inflate by.
+ public void Inflate(int width, int height)
+ {
+ Size += new RDSizeI(width * 2, height * 2);
+ Pivot -= (RDSizeI)new RDPointI(width, height);
+ }
+ ///
+ public static bool operator ==(RDRotatedRectI rect1, RDRotatedRectI rect2) => rect1.Equals(rect2);
+ ///
+ public static bool operator !=(RDRotatedRectI rect1, RDRotatedRectI rect2) => !rect1.Equals(rect2);
+ ///
+ public override readonly bool Equals([NotNullWhen(true)] object? obj) => obj is RDRotatedRectI e && Equals(e);
+ ///
+ public override readonly int GetHashCode() => HashCode.Combine(Location, Size, Pivot, Angle);
+ ///
+ public override readonly string ToString() => $"{{Location=[{Location}],Size=[{Size}],Pivot[{Pivot}],Angle={Angle}}}";
+ ///
+ public readonly bool Equals(RDRotatedRectI other) =>
+ Location == other.Location &&
+ Size == other.Size && Pivot
+ == other.Pivot &&
+ Angle == other.Angle;
+ private readonly string GetDebuggerDisplay() => ToString();
+ }
+}
\ No newline at end of file
diff --git a/RhythmBaseCore/Components/RDRotatedRectN.cs b/RhythmBaseCore/Components/RDRotatedRectN.cs
new file mode 100644
index 0000000..0c064c2
--- /dev/null
+++ b/RhythmBaseCore/Components/RDRotatedRectN.cs
@@ -0,0 +1,213 @@
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+
+namespace RhythmBase.Components
+{
+ ///
+ /// Represents a rotated rectangle with non-nullable float coordinates.
+ ///
+ [DebuggerDisplay($"{{{nameof(GetDebuggerDisplay)}(),nq}}")]
+ public struct RDRotatedRectN(RDPointN location, RDSizeN size, RDPointN pivot, float angle = 0) : IEquatable
+ {
+ ///
+ /// Gets or sets the location of the rectangle.
+ ///
+ public RDPointN Location { get; set; } = location;
+
+ ///
+ /// Gets or sets the size of the rectangle.
+ ///
+ public RDSizeN Size { get; set; } = size;
+
+ ///
+ /// Gets or sets the pivot point of the rotation.
+ ///
+ public RDPointN Pivot { get; set; } = pivot;
+
+ ///
+ /// Gets or sets the angle of rotation in radians.
+ ///
+ public float Angle { get; set; } = angle;
+
+ ///
+ /// Gets the top-left point of the rotated rectangle.
+ ///
+ public readonly RDPointN LeftTop => (Location - (RDSizeN)Pivot + new RDSizeN(0, Size.Height)).Rotate(Location, Angle);
+
+ ///
+ /// Gets the top-right point of the rotated rectangle.
+ ///
+ public readonly RDPointN RightTop => (Location - (RDSizeN)Pivot + Size).Rotate(Location, Angle);
+
+ ///
+ /// Gets the bottom-left point of the rotated rectangle.
+ ///
+ public readonly RDPointN LeftBottom => (Location - (RDSizeN)Pivot).Rotate(Location, Angle);
+
+ ///
+ /// Gets the bottom-right point of the rotated rectangle.
+ ///
+ public readonly RDPointN RightBottom => (Location - (RDSizeN)Pivot + new RDSizeN(Size.Width, 0)).Rotate(Location, Angle);
+
+ ///
+ /// Gets the rectangle without rotation.
+ ///
+ public readonly RDRectN WithoutRotate => new(Location - (RDSizeN)Pivot, Size);
+
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ /// The rectangle.
+ /// The pivot point.
+ /// The angle of rotation.
+ public RDRotatedRectN(RDRectN rect, RDPointN pivot, float angle) : this(rect.Location, rect.Size, pivot, angle) { }
+
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ /// The rectangle.
+ public RDRotatedRectN(RDRectN rect) : this(rect.Location, rect.Size, default, 0f) { }
+
+ ///
+ /// Inflates the specified rectangle by the specified size.
+ ///
+ /// The rectangle to inflate.
+ /// The size to inflate by.
+ /// The inflated rectangle.
+ public static RDRotatedRectN Inflate(RDRotatedRectN rect, RDSizeN size)
+ {
+ RDRotatedRectN result = rect;
+ result.Inflate(size);
+ return result;
+ }
+
+ ///
+ /// Inflates the specified rectangle by the specified width and height.
+ ///
+ /// The rectangle to inflate.
+ /// The width to inflate by.
+ /// The height to inflate by.
+ /// The inflated rectangle.
+ public static RDRotatedRectN Inflate(RDRotatedRectN rect, int x, int y)
+ {
+ RDRotatedRectN result = rect;
+ result.Inflate(x, y);
+ return result;
+ }
+
+ ///
+ /// Offsets the rectangle by the specified x and y values.
+ ///
+ /// The x value to offset by.
+ /// The y value to offset by.
+ public void Offset(float x, float y) => Location += (RDSizeN)new RDPointN(x, y);
+
+ ///
+ /// Offsets the rectangle by the specified point.
+ ///
+ /// The point to offset by.
+ public void Offset(RDPointN p) => Offset(p.X, p.Y);
+
+ ///
+ /// Inflates the rectangle by the specified size.
+ ///
+ /// The size to inflate by.
+ public void Inflate(RDSizeN size)
+ {
+ Size += new RDSizeN(size.Width * 2, size.Height * 2);
+ Pivot -= (RDSizeN)new RDPointN(size.Width, size.Height);
+ }
+
+ ///
+ /// Inflates the rectangle by the specified width and height.
+ ///
+ /// The width to inflate by.
+ /// The height to inflate by.
+ public void Inflate(float width, float height)
+ {
+ Size += new RDSizeN(width * 2, height * 2);
+ Pivot -= (RDSizeN)new RDPointN(width, height);
+ }
+
+ ///
+ /// Determines whether the rectangle contains the specified point.
+ ///
+ /// The x coordinate of the point.
+ /// The y coordinate of the point.
+ /// true if the rectangle contains the point; otherwise, false.
+ public readonly bool Contains(float x, float y) => WithoutRotate.Contains(new RDPointN(x, y).Rotate(-Angle));
+
+ ///
+ /// Determines whether the rectangle contains the specified point.
+ ///
+ /// The point.
+ /// true if the rectangle contains the point; otherwise, false.
+ public readonly bool Contains(RDPointN p) => WithoutRotate.Contains(p.Rotate(-Angle));
+
+ ///
+ /// Determines whether the rectangle contains the specified rotated rectangle.
+ ///
+ /// The rotated rectangle.
+ /// true if the rectangle contains the rotated rectangle; otherwise, false.
+ public readonly bool Contains(RDRotatedRectN rect) => Contains(rect.LeftTop) &&
+ Contains(rect.RightTop) &&
+ Contains(rect.LeftBottom) &&
+ Contains(rect.RightBottom);
+
+ ///
+ /// Determines whether the rectangle intersects with the specified rotated rectangle.
+ ///
+ /// The rotated rectangle.
+ /// true if the rectangle intersects with the rotated rectangle; otherwise, false.
+ public readonly bool IntersectsWith(RDRotatedRectN rect) => Contains(rect.LeftTop) ||
+ Contains(rect.RightTop) ||
+ Contains(rect.LeftBottom) ||
+ Contains(rect.RightBottom);
+
+ ///
+ /// Determines whether two rotated rectangles are equal.
+ ///
+ /// The first rotated rectangle.
+ /// The second rotated rectangle.
+ /// true if the rectangles are equal; otherwise, false.
+ public static bool operator ==(RDRotatedRectN rect1, RDRotatedRectN rect2) => rect1.Equals(rect2);
+
+ ///
+ /// Determines whether two rotated rectangles are not equal.
+ ///
+ /// The first rotated rectangle.
+ /// The second rotated rectangle.
+ /// true if the rectangles are not equal; otherwise, false.
+ public static bool operator !=(RDRotatedRectN rect1, RDRotatedRectN rect2) => !rect1.Equals(rect2);
+
+ ///
+ /// Determines whether the specified object is equal to the current object.
+ ///
+ /// The object to compare with the current object.
+ /// true if the specified object is equal to the current object; otherwise, false.
+ public override readonly bool Equals([NotNullWhen(true)] object? obj) => obj is RDRotatedRectN e && Equals(e);
+
+ ///
+ /// Serves as the default hash function.
+ ///
+ /// A hash code for the current object.
+ public override readonly int GetHashCode() => HashCode.Combine(Location, Size, Pivot, Angle);
+
+ ///
+ /// Returns a string that represents the current object.
+ ///
+ /// A string that represents the current object.
+ public override readonly string ToString() => $"{{Location=[{Location}],Size=[{Size}],Pivot[{Pivot}],Angle={Angle}}}";
+
+ ///
+ /// Determines whether the specified is equal to the current .
+ ///
+ /// The to compare with the current .
+ /// true if the specified is equal to the current ; otherwise, false.
+ public readonly bool Equals(RDRotatedRectN other) => Location == other.Location &&
+ Size == other.Size && Pivot
+ == other.Pivot &&
+ Angle == other.Angle;
+ private readonly string GetDebuggerDisplay() => ToString();
+ }
+}
diff --git a/RhythmBaseCore/Components/RDRotatedRectNI.cs b/RhythmBaseCore/Components/RDRotatedRectNI.cs
new file mode 100644
index 0000000..ba48a76
--- /dev/null
+++ b/RhythmBaseCore/Components/RDRotatedRectNI.cs
@@ -0,0 +1,185 @@
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+namespace RhythmBase.Components
+{
+ ///
+ /// Represents a rotated rectangle with non-integer coordinates.
+ ///
+ [DebuggerDisplay($"{{{nameof(GetDebuggerDisplay)}(),nq}}")]
+ public struct RDRotatedRectNI(RDPointNI location, RDSizeNI size, RDPointNI pivot, float angle) : IEquatable
+ {
+ ///
+ /// Gets or sets the location of the rectangle.
+ ///
+ public RDPointNI Location { get; set; } = location;
+
+ ///
+ /// Gets or sets the size of the rectangle.
+ ///
+ public RDSizeNI Size { get; set; } = size;
+
+ ///
+ /// Gets or sets the pivot point of the rotation.
+ ///
+ public RDPointNI Pivot { get; set; } = pivot;
+
+ ///
+ /// Gets or sets the angle of rotation in degrees.
+ ///
+ public float Angle { get; set; } = angle;
+
+ ///
+ /// Gets the top-left point of the rotated rectangle.
+ ///
+ public readonly RDPointN LeftTop => (Location - (RDSizeNI)Pivot + new RDSizeNI(0, Size.Height)).Rotate(Location, Angle);
+
+ ///
+ /// Gets the top-right point of the rotated rectangle.
+ ///
+ public readonly RDPointN RightTop => (Location - (RDSizeNI)Pivot + Size).Rotate(Location, Angle);
+
+ ///
+ /// Gets the bottom-left point of the rotated rectangle.
+ ///
+ public readonly RDPointN LeftBottom => (Location - (RDSizeNI)Pivot).Rotate(Location, Angle);
+
+ ///
+ /// Gets the bottom-right point of the rotated rectangle.
+ ///
+ public readonly RDPointN RightBottom => (Location - (RDSizeNI)Pivot + new RDSizeNI(Size.Width, 0)).Rotate(Location, Angle);
+
+ ///
+ /// Gets the rectangle without rotation.
+ ///
+ public readonly RDRectNI WithoutRotate => new(Location - (RDSizeNI)Pivot, Size);
+
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ /// The rectangle.
+ /// The pivot point.
+ /// The angle of rotation.
+ public RDRotatedRectNI(RDRectNI rect, RDPointNI pivot, float angle) : this(rect.Location, rect.Size, pivot, angle) { }
+
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ /// The rectangle.
+ public RDRotatedRectNI(RDRectNI rect) : this(rect.Location, rect.Size, default, 0f) { }
+
+ ///
+ /// Inflates the specified rectangle by the specified size.
+ ///
+ /// The rectangle to inflate.
+ /// The size to inflate by.
+ /// The inflated rectangle.
+ public static RDRotatedRectNI Inflate(RDRotatedRectNI rect, RDSizeNI size)
+ {
+ RDRotatedRectNI result = rect;
+ result.Inflate(size);
+ return result;
+ }
+
+ ///
+ /// Inflates the specified rectangle by the specified width and height.
+ ///
+ /// The rectangle to inflate.
+ /// The width to inflate by.
+ /// The height to inflate by.
+ /// The inflated rectangle.
+ public static RDRotatedRectNI Inflate(RDRotatedRectNI rect, int x, int y)
+ {
+ RDRotatedRectNI result = rect;
+ result.Inflate(x, y);
+ return result;
+ }
+
+ ///
+ /// Offsets the rectangle by the specified x and y values.
+ ///
+ /// The x value to offset by.
+ /// The y value to offset by.
+ public void Offset(int x, int y) => Location += (RDSizeNI)new RDPointNI(x, y);
+
+ ///
+ /// Offsets the rectangle by the specified point.
+ ///
+ /// The point to offset by.
+ public void Offset(RDPointNI p) => Offset(p.X, p.Y);
+
+ ///
+ /// Inflates the rectangle by the specified size.
+ ///
+ /// The size to inflate by.
+ public void Inflate(RDSizeNI size)
+ {
+ Size += new RDSizeNI(size.Width * 2, size.Height * 2);
+ Pivot -= (RDSizeNI)new RDPointNI(size.Width, size.Height);
+ }
+
+ ///
+ /// Inflates the rectangle by the specified width and height.
+ ///
+ /// The width to inflate by.
+ /// The height to inflate by.
+ public void Inflate(int width, int height)
+ {
+ Size += new RDSizeNI(width * 2, height * 2);
+ Pivot -= (RDSizeNI)new RDPointNI(width, height);
+ }
+
+ ///
+ /// Determines whether the rectangle contains the specified point.
+ ///
+ /// The x coordinate of the point.
+ /// The y coordinate of the point.
+ /// true if the rectangle contains the point; otherwise, false.
+ public readonly bool Contains(int x, int y) => WithoutRotate.Contains(new RDPointN((float)x, (float)y).Rotate(-Angle));
+
+ ///
+ /// Determines whether the rectangle contains the specified point.
+ ///
+ /// The point.
+ /// true if the rectangle contains the point; otherwise, false.
+ public readonly bool Contains(RDPointN p) => WithoutRotate.Contains(p.Rotate(-Angle));
+
+ ///
+ /// Determines whether the rectangle contains the specified rotated rectangle.
+ ///
+ /// The rotated rectangle.
+ /// true if the rectangle contains the rotated rectangle; otherwise, false.
+ public readonly bool Contains(RDRotatedRectNI rect) =>
+ Contains(rect.LeftTop) &&
+ Contains(rect.RightTop) &&
+ Contains(rect.LeftBottom) &&
+ Contains(rect.RightBottom);
+
+ ///
+ /// Determines whether the rectangle intersects with the specified rotated rectangle.
+ ///
+ /// The rotated rectangle.
+ /// true if the rectangle intersects with the rotated rectangle; otherwise, false.
+ public readonly bool IntersectsWith(RDRotatedRectNI rect) =>
+ Contains(rect.LeftTop) ||
+ Contains(rect.RightTop) ||
+ Contains(rect.LeftBottom) ||
+ Contains(rect.RightBottom);
+ ///
+ public static bool operator ==(RDRotatedRectNI rect1, RDRotatedRectNI rect2) => rect1.Equals(rect2);
+ ///
+ public static bool operator !=(RDRotatedRectNI rect1, RDRotatedRectNI rect2) => !rect1.Equals(rect2);
+ ///
+ public override readonly bool Equals([NotNullWhen(true)] object? obj) => obj is RDRotatedRectNI e && Equals(e);
+ ///
+ public override readonly int GetHashCode() => HashCode.Combine(Location, Size, Pivot, Angle);
+ ///
+ public override readonly string ToString() => $"{{Location=[{Location}],Size=[{Size}],Pivot[{Pivot}],Angle=[{Angle}]}}";
+ ///
+ public readonly bool Equals(RDRotatedRectNI other) =>
+ Location == other.Location &&
+ Size == other.Size && Pivot
+ == other.Pivot &&
+ Angle == other.Angle;
+ private readonly string GetDebuggerDisplay() => ToString();
+ }
+}
\ No newline at end of file
diff --git a/RhythmBaseCore/Components/RDSingleRoom.cs b/RhythmBaseCore/Components/RDSingleRoom.cs
new file mode 100644
index 0000000..8b3d142
--- /dev/null
+++ b/RhythmBaseCore/Components/RDSingleRoom.cs
@@ -0,0 +1,79 @@
+using Newtonsoft.Json;
+using RhythmBase.Converters;
+namespace RhythmBase.Components
+{
+ ///
+ /// Represents a single room that can be applied to one room only.
+ ///
+ [JsonConverter(typeof(RoomConverter))]
+ public struct RDSingleRoom(RDRoomIndex index) : IEquatable
+ {
+ ///
+ /// Gets a value indicating whether it can be used in the top room.
+ ///
+ public bool EnableTop { get; }
+
+ ///
+ /// Gets or sets the applied room.
+ ///
+ public RDRoomIndex Room
+ {
+ readonly get => _data;
+ set => _data = value;
+ }
+
+ ///
+ /// Gets or sets the applied room index as a byte.
+ ///
+ public byte Value
+ {
+ readonly get
+ {
+ for (int i = 0; i < 5; i++)
+ {
+ if (_data == (RDRoomIndex)(1 << i))
+ return (byte)i;
+ }
+ return byte.MaxValue;
+ }
+ set => _data = (RDRoomIndex)(1 << value);
+ }
+
+ ///
+ /// Returns a string that represents the current object.
+ ///
+ /// A string that represents the current object.
+ public override readonly string ToString() => string.Format("[{0}]", _data);
+
+ ///
+ /// Gets the default single room which represents room 0.
+ ///
+ public static RDSingleRoom Default => new((RDRoomIndex)255);
+
+ ///
+ /// Initializes a new instance of the struct with the specified room index.
+ ///
+ /// The room index.
+ public RDSingleRoom(byte room) : this((RDRoomIndex)(1 << (int)room)) { }
+
+ ///
+ public static bool operator ==(RDSingleRoom R1, RDSingleRoom R2) => R1._data == R2._data;
+
+ ///
+ public static bool operator !=(RDSingleRoom R1, RDSingleRoom R2) => R1._data != R2._data;
+
+ ///
+ public static implicit operator RDSingleRoom(RDRoomIndex room) => new(room);
+
+ ///
+ public override readonly bool Equals(object? obj) => obj is RDSingleRoom e && Equals(e);
+
+ ///
+ public override readonly int GetHashCode() => HashCode.Combine(_data);
+
+ ///
+ public readonly bool Equals(RDSingleRoom other) => _data == other._data;
+
+ private RDRoomIndex _data = index;
+ }
+}
diff --git a/RhythmBaseCore/Components/RDSize.cs b/RhythmBaseCore/Components/RDSize.cs
new file mode 100644
index 0000000..55c9e0d
--- /dev/null
+++ b/RhythmBaseCore/Components/RDSize.cs
@@ -0,0 +1,105 @@
+using Newtonsoft.Json;
+using RhythmBase.Converters;
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+namespace RhythmBase.Components
+{
+ ///
+ /// A size whose horizontal and vertical coordinates are nullable
+ ///
+ [JsonConverter(typeof(RDPointsConverter))]
+ [DebuggerDisplay($"{{{nameof(GetDebuggerDisplay)}(),nq}}")]
+ public struct RDSize(float? width, float? height) : IRDVortex
+ {
+ ///
+ /// Initializes a new instance of the struct with the specified point.
+ ///
+ /// The point to initialize the size with.
+ public RDSize(RDPoint pt) : this(pt.X, pt.Y) { }
+
+ ///
+ /// Gets a value indicating whether this size is empty (both width and height are null).
+ ///
+ public readonly bool IsEmpty => Width == null && Height == null;
+
+ ///
+ /// Gets or sets the width of the size.
+ ///
+ public float? Width { get; set; } = width;
+
+ ///
+ /// Gets or sets the height of the size.
+ ///
+ public float? Height { get; set; } = height;
+
+ ///
+ /// Gets the area of the size.
+ ///
+ public readonly float? Area => Width * Height;
+
+ ///
+ /// Adds two sizes together.
+ ///
+ /// The first size.
+ /// The second size.
+ /// The sum of the two sizes.
+ public static RDSize Add(RDSize sz1, RDSize sz2) => new(sz1.Width + sz2.Width, sz1.Height + sz2.Height);
+
+ ///
+ /// Subtracts one size from another.
+ ///
+ /// The first size.
+ /// The second size.
+ /// The difference between the two sizes.
+ public static RDSize Subtract(RDSize sz1, RDSize sz2) => new(sz1.Width - sz2.Width, sz1.Height - sz2.Height);
+ ///
+ public override readonly int GetHashCode() => HashCode.Combine(Width, Height);
+ ///
+ public override readonly string ToString() => $"[{Width},{Height}]";
+ ///
+ public readonly bool Equals(RDSize other) => Width == other.Width && Height == other.Height;
+
+ ///
+ /// Converts this size to an .
+ ///
+ /// An that represents this size.
+ public readonly RDSizeI ToSize() => new((int?)Width, (int?)Height);
+
+ ///
+ /// Converts this size to an .
+ ///
+ /// An that represents this size.
+ public readonly RDPoint ToPointF() => new(Width, Height);
+ ///
+ public override readonly bool Equals([NotNullWhen(true)] object? obj) => obj is RDSize e && Equals(e);
+ ///
+ public static RDSize operator +(RDSize sz1, RDSize sz2) => Add(sz1, sz2);
+ ///
+ public static RDSize operator -(RDSize sz1, RDSize sz2) => Subtract(sz1, sz2);
+ ///
+ public static RDSize operator *(float left, RDSize right) => new(left * right.Width, left * right.Height);
+ ///
+ public static RDSize operator *(RDSize left, float? right) => new(left.Width * right, left.Height * right);
+ ///
+ public static RDSize operator /(RDSize left, float? right) => new(left.Width / right, left.Height / right);
+ ///
+ public static bool operator ==(RDSize sz1, RDSize sz2) => sz1.Equals(sz2);
+ ///
+ public static bool operator !=(RDSize sz1, RDSize sz2) => !sz1.Equals(sz2);
+
+ ///
+ /// Performs an implicit conversion from to .
+ ///
+ /// The size to convert.
+ /// An that represents the converted size.
+ public static implicit operator RDSizeE(RDSize size) => new(size.Width, size.Height);
+
+ ///
+ /// Performs an explicit conversion from to .
+ ///
+ /// The size to convert.
+ /// An that represents the converted size.
+ public static explicit operator RDPoint(RDSize size) => new(size.Width, size.Height);
+ private readonly string GetDebuggerDisplay() => ToString();
+ }
+}
diff --git a/RhythmBaseCore/Components/RDSizeE.cs b/RhythmBaseCore/Components/RDSizeE.cs
new file mode 100644
index 0000000..1be5fd7
--- /dev/null
+++ b/RhythmBaseCore/Components/RDSizeE.cs
@@ -0,0 +1,204 @@
+using Newtonsoft.Json;
+using RhythmBase.Converters;
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+namespace RhythmBase.Components
+{
+ ///
+ /// A size whose horizontal and vertical coordinates are nullable
+ ///
+ [JsonConverter(typeof(RDPointsConverter))]
+ [DebuggerDisplay($"{{{nameof(GetDebuggerDisplay)}(),nq}}")]
+ public struct RDSizeE(RDExpression? width, RDExpression? height) :
+ IRDVortex,
+ IRDVortex,
+ IRDVortex
+ {
+ ///
+ /// Initializes a new instance of the struct with specified width and height as floats.
+ ///
+ /// The width as a float.
+ /// The height as a float.
+ public RDSizeE(float width, float height) : this((RDExpression)width, (RDExpression)height) { }
+
+ ///
+ /// Initializes a new instance of the struct with specified width as an RDExpression and height as a float.
+ ///
+ /// The width as an RDExpression.
+ /// The height as a float.
+ public RDSizeE(RDExpression? width, float height) : this(width, (RDExpression)height) { }
+
+ ///
+ /// Initializes a new instance of the struct with specified width as a float and height as an RDExpression.
+ ///
+ /// The width as a float.
+ /// The height as an RDExpression.
+ public RDSizeE(float width, RDExpression? height) : this((RDExpression)width, height) { }
+
+ ///
+ /// Initializes a new instance of the struct with specified width as a string and height as a float.
+ ///
+ /// The width as a string.
+ /// The height as a float.
+ public RDSizeE(string width, float height) : this((RDExpression)width, (RDExpression)height) { }
+
+ ///
+ /// Initializes a new instance of the struct with specified width as a float and height as a string.
+ ///
+ /// The width as a float.
+ /// The height as a string.
+ public RDSizeE(float width, string height) : this((RDExpression)width, (RDExpression)height) { }
+
+ ///
+ /// Initializes a new instance of the struct with specified width and height as strings.
+ ///
+ /// The width as a string.
+ /// The height as a string.
+ public RDSizeE(string width, string height) : this((RDExpression)width, (RDExpression)height) { }
+
+ ///
+ /// Initializes a new instance of the struct with specified width as a string and height as an RDExpression.
+ ///
+ /// The width as a string.
+ /// The height as an RDExpression.
+ public RDSizeE(string width, RDExpression? height) : this((RDExpression)width, height) { }
+
+ ///
+ /// Initializes a new instance of the struct with specified width as an RDExpression and height as a string.
+ ///
+ /// The width as an RDExpression.
+ /// The height as a string.
+ public RDSizeE(RDExpression? width, string height) : this(width, (RDExpression)height) { }
+
+ ///
+ /// Initializes a new instance of the struct from an instance.
+ ///
+ /// The instance.
+ public RDSizeE(RDSizeI p) : this((RDExpression?)p.Width, (RDExpression?)p.Height) { }
+
+ ///
+ /// Initializes a new instance of the struct from an instance.
+ ///
+ /// The instance.
+ public RDSizeE(RDSize p) : this((RDExpression?)p.Width, (RDExpression?)p.Height) { }
+
+ ///
+ /// Initializes a new instance of the struct from an instance.
+ ///
+ /// The instance.
+ public RDSizeE(RDPointI p) : this((RDExpression?)p.X, (RDExpression?)p.Y) { }
+
+ ///
+ /// Initializes a new instance of the struct from an instance.
+ ///
+ /// The instance.
+ public RDSizeE(RDPoint p) : this((RDExpression?)p.X, (RDExpression?)p.Y) { }
+
+ ///
+ /// Initializes a new instance of the struct from an instance.
+ ///
+ /// The instance.
+ public RDSizeE(RDPointE p) : this(p.X, p.Y) { }
+
+ ///
+ /// Gets a value indicating whether this instance is empty.
+ ///
+ public readonly bool IsEmpty => Width == null && Height == null;
+
+ ///
+ /// Gets or sets the width.
+ ///
+ public RDExpression? Width { get; set; } = width;
+
+ ///
+ /// Gets or sets the height.
+ ///
+ public RDExpression? Height { get; set; } = height;
+
+ ///
+ /// Gets the area of the size.
+ ///
+ public readonly RDExpression? Area => Width * Height;
+
+ ///
+ /// Adds two instances and returns the result.
+ ///
+ /// The first instance.
+ /// The second instance.
+ /// The result of the addition.
+ public static RDSizeE Add(RDSizeE sz1, RDSize sz2) => new(sz1.Width + sz2.Width, sz1.Height + sz2.Height);
+
+ ///
+ /// Adds two instances and returns the result.
+ ///
+ /// The first instance.
+ /// The second instance.
+ /// The result of the addition.
+ public static RDSizeE Add(RDSizeE sz1, RDSizeE sz2) => new(sz1.Width + sz2.Width, sz1.Height + sz2.Height);
+
+ ///
+ /// Subtracts one instance from another instance and returns the result.
+ ///
+ /// The first instance.
+ /// The second instance.
+ /// The result of the subtraction.
+ public static RDSizeE Subtract(RDSizeE sz1, RDSize sz2) => new(sz1.Width - sz2.Width, sz1.Height - sz2.Height);
+
+ ///
+ /// Subtracts one instance from another instance and returns the result.
+ ///
+ /// The first instance.
+ /// The second instance.
+ /// The result of the subtraction.
+ public static RDSizeE Subtract(RDSizeE sz1, RDSizeE sz2) => new(sz1.Width - sz2.Width, sz1.Height - sz2.Height);
+ ///
+ public override readonly int GetHashCode() => HashCode.Combine(Width, Height);
+ ///
+ public override readonly string ToString() => $"[{Width},{Height}]";
+ ///
+ public readonly bool Equals(RDSizeE other) => Width == other.Width && Height == other.Height;
+ ///
+ public readonly RDPointE ToRDPointE() => new(Width, Height);
+ ///
+ public override readonly bool Equals([NotNullWhen(true)] object? obj) => obj is RDSize e && Equals(e);
+ ///
+ public static RDSizeE operator +(RDSizeE sz1, RDSizeI sz2) => Add(sz1, sz2);
+ ///
+ public static RDSizeE operator +(RDSizeE sz1, RDSize sz2) => Add(sz1, sz2);
+ ///
+ public static RDSizeE operator +(RDSizeE sz1, RDSizeE sz2) => Add(sz1, sz2);
+ ///
+ public static RDSizeE operator -(RDSizeE sz1, RDSizeI sz2) => Subtract(sz1, sz2);
+ ///
+ public static RDSizeE operator -(RDSizeE sz1, RDSize sz2) => Subtract(sz1, sz2);
+ ///
+ public static RDSizeE operator -(RDSizeE sz1, RDSizeE sz2) => Subtract(sz1, sz2);
+ ///
+ public static RDSizeE operator *(int left, RDSizeE right) => new(left * right.Width, left * right.Height);
+ ///
+ public static RDSizeE operator *(RDSizeE left, int right) => new(left.Width * right, left.Height * right);
+ ///
+ public static RDSizeE operator *(float left, RDSizeE right) => new(left * right.Width, left * right.Height);
+ ///
+ public static RDSizeE operator *(RDSizeE left, float right) => new(left.Width * right, left.Height * right);
+ ///
+ public static RDSizeE operator *(RDExpression left, RDSizeE right) => new(left * right.Width, left * right.Height);
+ ///
+ public static RDSizeE operator *(RDSizeE left, RDExpression right) => new(left.Width * right, left.Height * right);
+ ///
+ public static RDSizeE operator /(RDSizeE left, float right) => new(left.Width / right, left.Height / right);
+ ///
+ public static RDSizeE operator /(RDSizeE left, RDExpression right) => new(left.Width / right, left.Height / right);
+ ///
+ public static bool operator ==(RDSizeE sz1, RDSizeE sz2) => sz1.Equals(sz2);
+ ///
+ public static bool operator !=(RDSizeE sz1, RDSizeE sz2) => !sz1.Equals(sz2);
+ ///
+ /// Converts an instance to an instance explicitly.
+ ///
+ /// The instance to convert.
+ /// An instance with the same width and height as the instance.
+ public static explicit operator RDPointE(RDSizeE size) => new(size.Width, size.Height);
+ private readonly string GetDebuggerDisplay() => ToString();
+ }
+}
diff --git a/RhythmBaseCore/Components/RDSizeI.cs b/RhythmBaseCore/Components/RDSizeI.cs
new file mode 100644
index 0000000..2161148
--- /dev/null
+++ b/RhythmBaseCore/Components/RDSizeI.cs
@@ -0,0 +1,130 @@
+using Newtonsoft.Json;
+using RhythmBase.Converters;
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+namespace RhythmBase.Components
+{
+ ///
+ /// A size whose horizontal and vertical coordinates are nullable
+ ///
+ [JsonConverter(typeof(RDPointsConverter))]
+ [DebuggerDisplay($"{{{nameof(GetDebuggerDisplay)}(),nq}}")]
+ public struct RDSizeI(int? width, int? height) : IRDVortex
+ {
+ ///
+ /// Initializes a new instance of the