From 59a9043d975ccbd2ae2831351318fcfbb745652d Mon Sep 17 00:00:00 2001 From: Oliver Booth Date: Tue, 18 Jun 2024 15:45:49 +0100 Subject: [PATCH] feat: add action (de)serialization --- .../VpSharp.Building.Tests.csproj | 30 ++ VpSharp.Building.Tests/src/ColorTests.cs | 44 ++ VpSharp.Building.Tests/src/SetupTrace.cs | 19 + VpSharp.Building/VpSharp.Building.csproj | 15 + .../src/ActionSerializerOptions.cs | 96 ++++ VpSharp.Building/src/Assembly.cs | 3 + VpSharp.Building/src/BooleanMode.cs | 22 + VpSharp.Building/src/Command.cs | 86 +++ VpSharp.Building/src/CommandAttribute.cs | 35 ++ .../src/Extensions/TextReaderExtensions.cs | 29 ++ .../VirtualParadiseActionExtensions.cs | 195 +++++++ VpSharp.Building/src/FlagAttribute.cs | 46 ++ VpSharp.Building/src/LightEffect.cs | 45 ++ VpSharp.Building/src/LightType.cs | 19 + VpSharp.Building/src/ParameterAttribute.cs | 29 ++ VpSharp.Building/src/PropertyAttribute.cs | 35 ++ .../Serialization/ActionSerializer.Read.cs | 491 ++++++++++++++++++ .../Serialization/ActionSerializer.Write.cs | 367 +++++++++++++ .../src/Serialization/ActionSerializer.cs | 39 ++ .../Serialization/CommandConverter.Generic.cs | 56 ++ .../src/Serialization/CommandConverter.cs | 132 +++++ .../CommandConverterAttribute.Generic.cs | 16 + .../CommandConverterAttribute.cs | 37 ++ .../src/Serialization/TokenType.cs | 57 ++ .../src/Serialization/Utf8ActionWriter.cs | 116 +++++ .../Serialization/ValueConverter.Generic.cs | 55 ++ .../src/Serialization/ValueConverter.cs | 53 ++ .../ValueConverterAttribute.Generic.cs | 17 + .../Serialization/ValueConverterAttribute.cs | 45 ++ VpSharp.Building/src/TextAlignment.cs | 25 + VpSharp.Building/src/Trigger.cs | 55 ++ VpSharp.Building/src/TriggerAttribute.cs | 35 ++ .../ValueConverters/BooleanValueConverter.cs | 38 ++ .../src/ValueConverters/ByteValueConverter.cs | 23 + .../src/ValueConverters/CharValueConverter.cs | 21 + .../ValueConverters/ColorValueConverter.cs | 38 ++ .../ValueConverters/DecimalValueConverter.cs | 23 + .../ValueConverters/DoubleValueConverter.cs | 23 + .../ValueConverters/Int16ValueConverter.cs | 23 + .../ValueConverters/Int32ValueConverter.cs | 23 + .../ValueConverters/Int64ValueConverter.cs | 23 + .../ValueConverters/SByteValueConverter.cs | 24 + .../ValueConverters/SingleValueConverter.cs | 23 + .../ValueConverters/StringValueConverter.cs | 77 +++ .../ValueConverters/UInt16ValueConverter.cs | 24 + .../ValueConverters/UInt32ValueConverter.cs | 24 + .../ValueConverters/UInt64ValueConverter.cs | 24 + .../ValueConverters/Vector3ValueConverter.cs | 45 ++ VpSharp.Building/src/VirtualParadiseAction.cs | 171 ++++++ .../src/VirtualParadiseActionBuilder.cs | 119 +++++ VpSharp.Building/src/VirtualParadiseColors.cs | 308 +++++++++++ VpSharp.sln | 12 + 52 files changed, 3430 insertions(+) create mode 100644 VpSharp.Building.Tests/VpSharp.Building.Tests.csproj create mode 100644 VpSharp.Building.Tests/src/ColorTests.cs create mode 100644 VpSharp.Building.Tests/src/SetupTrace.cs create mode 100644 VpSharp.Building/VpSharp.Building.csproj create mode 100644 VpSharp.Building/src/ActionSerializerOptions.cs create mode 100644 VpSharp.Building/src/Assembly.cs create mode 100644 VpSharp.Building/src/BooleanMode.cs create mode 100644 VpSharp.Building/src/Command.cs create mode 100644 VpSharp.Building/src/CommandAttribute.cs create mode 100644 VpSharp.Building/src/Extensions/TextReaderExtensions.cs create mode 100644 VpSharp.Building/src/Extensions/VirtualParadiseActionExtensions.cs create mode 100644 VpSharp.Building/src/FlagAttribute.cs create mode 100644 VpSharp.Building/src/LightEffect.cs create mode 100644 VpSharp.Building/src/LightType.cs create mode 100644 VpSharp.Building/src/ParameterAttribute.cs create mode 100644 VpSharp.Building/src/PropertyAttribute.cs create mode 100644 VpSharp.Building/src/Serialization/ActionSerializer.Read.cs create mode 100644 VpSharp.Building/src/Serialization/ActionSerializer.Write.cs create mode 100644 VpSharp.Building/src/Serialization/ActionSerializer.cs create mode 100644 VpSharp.Building/src/Serialization/CommandConverter.Generic.cs create mode 100644 VpSharp.Building/src/Serialization/CommandConverter.cs create mode 100644 VpSharp.Building/src/Serialization/CommandConverterAttribute.Generic.cs create mode 100644 VpSharp.Building/src/Serialization/CommandConverterAttribute.cs create mode 100644 VpSharp.Building/src/Serialization/TokenType.cs create mode 100644 VpSharp.Building/src/Serialization/Utf8ActionWriter.cs create mode 100644 VpSharp.Building/src/Serialization/ValueConverter.Generic.cs create mode 100644 VpSharp.Building/src/Serialization/ValueConverter.cs create mode 100644 VpSharp.Building/src/Serialization/ValueConverterAttribute.Generic.cs create mode 100644 VpSharp.Building/src/Serialization/ValueConverterAttribute.cs create mode 100644 VpSharp.Building/src/TextAlignment.cs create mode 100644 VpSharp.Building/src/Trigger.cs create mode 100644 VpSharp.Building/src/TriggerAttribute.cs create mode 100644 VpSharp.Building/src/ValueConverters/BooleanValueConverter.cs create mode 100644 VpSharp.Building/src/ValueConverters/ByteValueConverter.cs create mode 100644 VpSharp.Building/src/ValueConverters/CharValueConverter.cs create mode 100644 VpSharp.Building/src/ValueConverters/ColorValueConverter.cs create mode 100644 VpSharp.Building/src/ValueConverters/DecimalValueConverter.cs create mode 100644 VpSharp.Building/src/ValueConverters/DoubleValueConverter.cs create mode 100644 VpSharp.Building/src/ValueConverters/Int16ValueConverter.cs create mode 100644 VpSharp.Building/src/ValueConverters/Int32ValueConverter.cs create mode 100644 VpSharp.Building/src/ValueConverters/Int64ValueConverter.cs create mode 100644 VpSharp.Building/src/ValueConverters/SByteValueConverter.cs create mode 100644 VpSharp.Building/src/ValueConverters/SingleValueConverter.cs create mode 100644 VpSharp.Building/src/ValueConverters/StringValueConverter.cs create mode 100644 VpSharp.Building/src/ValueConverters/UInt16ValueConverter.cs create mode 100644 VpSharp.Building/src/ValueConverters/UInt32ValueConverter.cs create mode 100644 VpSharp.Building/src/ValueConverters/UInt64ValueConverter.cs create mode 100644 VpSharp.Building/src/ValueConverters/Vector3ValueConverter.cs create mode 100644 VpSharp.Building/src/VirtualParadiseAction.cs create mode 100644 VpSharp.Building/src/VirtualParadiseActionBuilder.cs create mode 100644 VpSharp.Building/src/VirtualParadiseColors.cs diff --git a/VpSharp.Building.Tests/VpSharp.Building.Tests.csproj b/VpSharp.Building.Tests/VpSharp.Building.Tests.csproj new file mode 100644 index 0000000..6cd981a --- /dev/null +++ b/VpSharp.Building.Tests/VpSharp.Building.Tests.csproj @@ -0,0 +1,30 @@ + + + + net6.0;net8.0 + enable + enable + + false + true + + CS1591 + + + + + + + + + + + + + + + + + + + diff --git a/VpSharp.Building.Tests/src/ColorTests.cs b/VpSharp.Building.Tests/src/ColorTests.cs new file mode 100644 index 0000000..2ce1d6d --- /dev/null +++ b/VpSharp.Building.Tests/src/ColorTests.cs @@ -0,0 +1,44 @@ +using System.Drawing; + +namespace VpSharp.Building.Tests; + +[TestFixture] +internal class ColorTests +{ + [Test] + public void VirtualParadiseColors_ShouldBeDefinedCorrectly() + { + Assert.Multiple(() => + { + Assert.That(VirtualParadiseColors.Aquamarine, Is.EqualTo(Color.FromArgb(0x70, 0xDB, 0x93))); + Assert.That(VirtualParadiseColors.Black, Is.EqualTo(Color.FromArgb(0x00, 0x00, 0x00))); + Assert.That(VirtualParadiseColors.Blue, Is.EqualTo(Color.FromArgb(0x00, 0x00, 0xFF))); + Assert.That(VirtualParadiseColors.Copper, Is.EqualTo(Color.FromArgb(0xB8, 0x73, 0x33))); + Assert.That(VirtualParadiseColors.Cyan, Is.EqualTo(Color.FromArgb(0x00, 0xFF, 0xFF))); + Assert.That(VirtualParadiseColors.DarkGrey, Is.EqualTo(Color.FromArgb(0x30, 0x30, 0x30))); + Assert.That(VirtualParadiseColors.DefaultSignBackColor, Is.EqualTo(Color.FromArgb(0x00, 0x00, 0xC0))); + Assert.That(VirtualParadiseColors.ForestGreen, Is.EqualTo(Color.FromArgb(0x23, 0x8E, 0x23))); + Assert.That(VirtualParadiseColors.Gold, Is.EqualTo(Color.FromArgb(0xCD, 0x7F, 0x32))); + Assert.That(VirtualParadiseColors.Green, Is.EqualTo(Color.FromArgb(0x00, 0xFF, 0x00))); + Assert.That(VirtualParadiseColors.Grey, Is.EqualTo(Color.FromArgb(0x70, 0x70, 0x70))); + Assert.That(VirtualParadiseColors.LightGrey, Is.EqualTo(Color.FromArgb(0xC0, 0xC0, 0xC0))); + Assert.That(VirtualParadiseColors.Magenta, Is.EqualTo(Color.FromArgb(0xFF, 0x00, 0xFF))); + Assert.That(VirtualParadiseColors.Maroon, Is.EqualTo(Color.FromArgb(0x8E, 0x23, 0x6B))); + Assert.That(VirtualParadiseColors.Orange, Is.EqualTo(Color.FromArgb(0xFF, 0x7F, 0x00))); + Assert.That(VirtualParadiseColors.OrangeRed, Is.EqualTo(Color.FromArgb(0xFF, 0x24, 0x00))); + Assert.That(VirtualParadiseColors.Pink, Is.EqualTo(Color.FromArgb(0xFF, 0x6E, 0xC7))); + Assert.That(VirtualParadiseColors.Red, Is.EqualTo(Color.FromArgb(0xFF, 0x00, 0x00))); + Assert.That(VirtualParadiseColors.Salmon, Is.EqualTo(Color.FromArgb(0x6F, 0x42, 0x42))); + Assert.That(VirtualParadiseColors.Scarlet, Is.EqualTo(Color.FromArgb(0x8C, 0x17, 0x17))); + Assert.That(VirtualParadiseColors.Silver, Is.EqualTo(Color.FromArgb(0xE6, 0xE8, 0xFA))); + Assert.That(VirtualParadiseColors.SkyBlue, Is.EqualTo(Color.FromArgb(0x32, 0x99, 0xCC))); + Assert.That(VirtualParadiseColors.Tan, Is.EqualTo(Color.FromArgb(0xDB, 0x93, 0x70))); + Assert.That(VirtualParadiseColors.Teal, Is.EqualTo(Color.FromArgb(0x00, 0x70, 0x70))); + Assert.That(VirtualParadiseColors.Transparent, Is.EqualTo(Color.FromArgb(0x00, 0x00, 0x00, 0x00))); + Assert.That(VirtualParadiseColors.Turquoise, Is.EqualTo(Color.FromArgb(0xAD, 0xEA, 0xEA))); + Assert.That(VirtualParadiseColors.Violet, Is.EqualTo(Color.FromArgb(0x4F, 0x2F, 0x4F))); + Assert.That(VirtualParadiseColors.White, Is.EqualTo(Color.FromArgb(0xFF, 0xFF, 0xFF))); + Assert.That(VirtualParadiseColors.Yellow, Is.EqualTo(Color.FromArgb(0xFF, 0xFF, 0x00))); + }); + } +} diff --git a/VpSharp.Building.Tests/src/SetupTrace.cs b/VpSharp.Building.Tests/src/SetupTrace.cs new file mode 100644 index 0000000..a00aa42 --- /dev/null +++ b/VpSharp.Building.Tests/src/SetupTrace.cs @@ -0,0 +1,19 @@ +using System.Diagnostics; + +namespace VpSharp.Building.Tests; + +[SetUpFixture] +internal class SetupTrace +{ + [OneTimeSetUp] + public void OneTimeSetUp() + { + Trace.Listeners.Add(new ConsoleTraceListener()); + } + + [OneTimeTearDown] + public void OneTimeTearDown() + { + Trace.Flush(); + } +} diff --git a/VpSharp.Building/VpSharp.Building.csproj b/VpSharp.Building/VpSharp.Building.csproj new file mode 100644 index 0000000..e2732b4 --- /dev/null +++ b/VpSharp.Building/VpSharp.Building.csproj @@ -0,0 +1,15 @@ + + + + net6.0;net8.0 + enable + enable + + + + + + + + + diff --git a/VpSharp.Building/src/ActionSerializerOptions.cs b/VpSharp.Building/src/ActionSerializerOptions.cs new file mode 100644 index 0000000..162f4fd --- /dev/null +++ b/VpSharp.Building/src/ActionSerializerOptions.cs @@ -0,0 +1,96 @@ +using VpSharp.Building.Serialization; + +namespace VpSharp.Building; + +/// +/// Defines options for serialization Virtual Paradise action strings. +/// +public sealed class ActionSerializerOptions +{ + /// + /// Gets or initializes the boolean literal mode. + /// + /// The boolean literal mode. Defaults to . + public BooleanMode BooleanMode { get; init; } = BooleanMode.OnOff; + + /// + /// Gets or initializes the custom command converters. + /// + /// The custom command converters. + public ICollection CustomCommandConverters { get; init; } = []; + + /// + /// Gets or initializes the custom command types. + /// + /// The custom command types. + public ICollection CustomCommands { get; init; } = []; + + /// + /// Gets or initializes the custom triggers types. + /// + /// The custom triggers types. + public ICollection CustomTriggers { get; init; } = []; + + /// + /// Gets or initializes the custom value converters. + /// + /// The custom value converters. + public ICollection CustomValueConverters { get; init; } = []; + + internal ICollection CommandConverters + { + get => [..CustomCommandConverters, ..GetInternalCommandConverters([])]; + } + + internal ICollection Commands + { + get => [..CustomCommands, ..GetInternalCommands([])]; + } + + internal ICollection Triggers + { + get => [..CustomTriggers, ..GetInternalTriggers([])]; + } + + internal ICollection ValueConverters + { + get => [..CustomValueConverters, ..GetInternalValueConverters([])]; + } + + + private static Type[] GetInternalCommandConverters(IEnumerable except) + { + return typeof(CommandConverter).Assembly + .GetTypes() + .Except(except) + .Where(t => !t.IsAbstract && t.IsSubclassOf(typeof(CommandConverter))) + .ToArray(); + } + + private static Type[] GetInternalCommands(IEnumerable except) + { + return typeof(Command).Assembly + .GetTypes() + .Except(except) + .Where(t => !t.IsAbstract && t.IsSubclassOf(typeof(Command))) + .ToArray(); + } + + private static Type[] GetInternalTriggers(IEnumerable except) + { + return typeof(Trigger).Assembly + .GetTypes() + .Except(except) + .Where(t => !t.IsAbstract && t.IsSubclassOf(typeof(Trigger))) + .ToArray(); + } + + private static Type[] GetInternalValueConverters(IEnumerable except) + { + return typeof(ValueConverter).Assembly + .GetTypes() + .Except(except) + .Where(t => !t.IsAbstract && t.IsSubclassOf(typeof(ValueConverter))) + .ToArray(); + } +} diff --git a/VpSharp.Building/src/Assembly.cs b/VpSharp.Building/src/Assembly.cs new file mode 100644 index 0000000..dca0be3 --- /dev/null +++ b/VpSharp.Building/src/Assembly.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("VpSharp.Building.Tests")] diff --git a/VpSharp.Building/src/BooleanMode.cs b/VpSharp.Building/src/BooleanMode.cs new file mode 100644 index 0000000..cb6beb1 --- /dev/null +++ b/VpSharp.Building/src/BooleanMode.cs @@ -0,0 +1,22 @@ +namespace VpSharp.Building; + +/// +/// An enumeration of boolean modes. +/// +public enum BooleanMode +{ + /// + /// Booleans will be written with the literals on and off. + /// + OnOff, + + /// + /// Booleans will be written with the literals yes and no. + /// + YesNo, + + /// + /// Booleans will be written with the literals 1 and 0. + /// + OneZero +} diff --git a/VpSharp.Building/src/Command.cs b/VpSharp.Building/src/Command.cs new file mode 100644 index 0000000..9f21795 --- /dev/null +++ b/VpSharp.Building/src/Command.cs @@ -0,0 +1,86 @@ +using System.Reflection; +using VpSharp.Building.Serialization; + +namespace VpSharp.Building; + +/// +/// Represents a command. +/// +public abstract class Command +{ + /// + /// Gets the flags for this command. + /// + /// The flags. + public IReadOnlyList Flags { get; internal set; } = []; + + /// + /// Gets the properties for this command. + /// + /// The properties. + public Dictionary Properties { get; internal set; } = new(StringComparer.OrdinalIgnoreCase); + + /// + /// Gets the raw arguments for this command. + /// + /// The raw arguments. + public IReadOnlyList RawArguments { get; internal set; } = []; + + /// + /// Gets the raw argument string for this command. + /// + /// The raw argument string. + public string RawArgumentString { get; internal set; } = string.Empty; + + /// + /// Gets or sets the target of the command. + /// + /// The name of the target object. + /// The target is defined by the name property. + [Property("name")] + public string? Name + { + get => Properties.GetValueOrDefault("name"); + set + { + if (string.IsNullOrWhiteSpace(value)) + { + Properties.Remove("name"); + } + else + { + Properties["name"] = value; + } + } + } + + internal void Poll(ActionSerializerOptions options) + { + foreach (PropertyInfo member in GetType().GetProperties()) + { + if (member.GetCustomAttribute() is not { } attribute) + { + continue; + } + + if (!Properties.TryGetValue(attribute.PropertyName, out string? propertyValue)) + { + continue; + } + + using var reader = new StringReader(propertyValue); + foreach (Type converterType in options.ValueConverters) + { + var converter = (ValueConverter)Activator.CreateInstance(converterType)!; + if (!converter.CanConvert(member.PropertyType)) + { + continue; + } + + object? value = converter.Read(reader, member.PropertyType, options); + member.SetValue(this, value); + break; + } + } + } +} diff --git a/VpSharp.Building/src/CommandAttribute.cs b/VpSharp.Building/src/CommandAttribute.cs new file mode 100644 index 0000000..d0d4d9d --- /dev/null +++ b/VpSharp.Building/src/CommandAttribute.cs @@ -0,0 +1,35 @@ +namespace VpSharp.Building; + +/// +/// Defines the name of a command. +/// +[AttributeUsage(AttributeTargets.Class)] +public sealed class CommandAttribute : Attribute +{ + /// + /// Initializes a new instance of the class. + /// + /// The name of the command. + /// is . + /// is empty, or consists only of whitespace. + public CommandAttribute(string commandName) + { + if (commandName is null) + { + throw new ArgumentNullException(nameof(commandName)); + } + + if (string.IsNullOrWhiteSpace(commandName)) + { + throw new ArgumentException("Command name cannot be empty", nameof(commandName)); + } + + CommandName = commandName; + } + + /// + /// Gets the name of the command. + /// + /// The name of the command. + public string CommandName { get; } +} diff --git a/VpSharp.Building/src/Extensions/TextReaderExtensions.cs b/VpSharp.Building/src/Extensions/TextReaderExtensions.cs new file mode 100644 index 0000000..cf60309 --- /dev/null +++ b/VpSharp.Building/src/Extensions/TextReaderExtensions.cs @@ -0,0 +1,29 @@ +using Cysharp.Text; + +namespace VpSharp.Building.Extensions; + +internal static class TextReaderExtensions +{ + public static string ReadToSpace(this TextReader reader) + { + using Utf16ValueStringBuilder builder = ZString.CreateStringBuilder(); + + while (true) + { + int current = reader.Read(); + if (current == -1) + { + break; + } + + if (char.IsWhiteSpace((char)current)) + { + break; + } + + builder.Append((char)current); + } + + return builder.ToString(); + } +} diff --git a/VpSharp.Building/src/Extensions/VirtualParadiseActionExtensions.cs b/VpSharp.Building/src/Extensions/VirtualParadiseActionExtensions.cs new file mode 100644 index 0000000..0c20973 --- /dev/null +++ b/VpSharp.Building/src/Extensions/VirtualParadiseActionExtensions.cs @@ -0,0 +1,195 @@ +using System.Diagnostics.CodeAnalysis; +using VpSharp.Building.Triggers; + +namespace VpSharp.Building.Extensions; + +/// +/// Extension methods for . +/// +public static class VirtualParadiseActionExtensions +{ + /// + /// Returns the activate trigger. + /// + /// The action whose triggers to search. + /// The activate trigger from this action, or if no such trigger exists. + /// is . + public static ActivateTrigger? Activate(this VirtualParadiseAction action) + { + if (action is null) + { + throw new ArgumentNullException(nameof(action)); + } + + return action.GetTrigger(); + } + + /// + /// Returns the adone trigger. + /// + /// The action whose triggers to search. + /// The adone trigger from this action, or if no such trigger exists. + /// is . + public static ADoneTrigger? ADone(this VirtualParadiseAction action) + { + if (action is null) + { + throw new ArgumentNullException(nameof(action)); + } + + return action.GetTrigger(); + } + + /// + /// Returns the bump trigger. + /// + /// The action whose triggers to search. + /// The bump trigger from this action, or if no such trigger exists. + /// is . + public static BumpTrigger? Bump(this VirtualParadiseAction action) + { + if (action is null) + { + throw new ArgumentNullException(nameof(action)); + } + + return action.GetTrigger(); + } + + /// + /// Returns the bumpend trigger. + /// + /// The action whose triggers to search. + /// The bumpend trigger from this action, or if no such trigger exists. + /// is . + public static BumpEndTrigger? BumpEnd(this VirtualParadiseAction action) + { + if (action is null) + { + throw new ArgumentNullException(nameof(action)); + } + + return action.GetTrigger(); + } + + /// + /// Returns the create trigger. + /// + /// The action whose triggers to search. + /// The create trigger from this action, or if no such trigger exists. + /// is . + public static CreateTrigger? Create(this VirtualParadiseAction action) + { + if (action is null) + { + throw new ArgumentNullException(nameof(action)); + } + + return action.GetTrigger(); + } + + /// + /// Returns the activate trigger. + /// + /// The action whose triggers to search. + /// + /// When this method returns, contains the activate trigger from this action, or if no such + /// trigger exists. + /// + /// if the trigger was found; otherwise, . + /// is . + public static bool TryGetActivate(this VirtualParadiseAction action, [NotNullWhen(true)] out ActivateTrigger? trigger) + { + if (action is null) + { + throw new ArgumentNullException(nameof(action)); + } + + trigger = action.GetTrigger(); + return trigger is not null; + } + + /// + /// Returns the adone trigger. + /// + /// The action whose triggers to search. + /// + /// When this method returns, contains the adone trigger from this action, or if no such + /// trigger exists. + /// + /// if the trigger was found; otherwise, . + /// is . + public static bool TryGetADone(this VirtualParadiseAction action, [NotNullWhen(true)] out ADoneTrigger? trigger) + { + if (action is null) + { + throw new ArgumentNullException(nameof(action)); + } + + trigger = action.GetTrigger(); + return trigger is not null; + } + + /// + /// Returns the bump trigger. + /// + /// The action whose triggers to search. + /// + /// When this method returns, contains the bump trigger from this action, or if no such + /// trigger exists. + /// + /// if the trigger was found; otherwise, . + /// is . + public static bool TryGetBump(this VirtualParadiseAction action, [NotNullWhen(true)] out BumpTrigger? trigger) + { + if (action is null) + { + throw new ArgumentNullException(nameof(action)); + } + + trigger = action.GetTrigger(); + return trigger is not null; + } + + /// + /// Returns the bumpend trigger. + /// + /// The action whose triggers to search. + /// + /// When this method returns, contains the bumpend trigger from this action, or if no such + /// trigger exists. + /// + /// if the trigger was found; otherwise, . + /// is . + public static bool TryGetBumpEnd(this VirtualParadiseAction action, [NotNullWhen(true)] out BumpEndTrigger? trigger) + { + if (action is null) + { + throw new ArgumentNullException(nameof(action)); + } + + trigger = action.GetTrigger(); + return trigger is not null; + } + + /// + /// Returns the create trigger. + /// + /// The action whose triggers to search. + /// + /// When this method returns, contains the create trigger from this action, or if no such + /// trigger exists. + /// + /// if the trigger was found; otherwise, . + /// is . + public static bool TryGetCreate(this VirtualParadiseAction action, [NotNullWhen(true)] out CreateTrigger? trigger) + { + if (action is null) + { + throw new ArgumentNullException(nameof(action)); + } + + trigger = action.GetTrigger(); + return trigger is not null; + } +} diff --git a/VpSharp.Building/src/FlagAttribute.cs b/VpSharp.Building/src/FlagAttribute.cs new file mode 100644 index 0000000..14894b2 --- /dev/null +++ b/VpSharp.Building/src/FlagAttribute.cs @@ -0,0 +1,46 @@ +namespace VpSharp.Building; + +/// +/// Defines a flag. +/// +public sealed class FlagAttribute : Attribute +{ + /// + /// Initializes a new instance of the class. + /// + /// The name of the flag. + /// is . + /// is empty or consists of only whitespace. + public FlagAttribute(string name) + { + if (name is null) + { + throw new ArgumentNullException(nameof(name)); + } + + if (string.IsNullOrWhiteSpace(name)) + { + throw new ArgumentException("Flag name cannot be empty.", nameof(name)); + } + + Name = name; + } + + /// + /// Gets or sets the default value of the flag. + /// + /// The default value. + public bool DefaultValue { get; set; } = false; + + /// + /// Gets or sets a value indicating whether this flag is optional. + /// + /// if this flag is optional; otherwise, . + public bool IsOptional { get; set; } = true; + + /// + /// Gets the name of the flag. + /// + /// The name of the flag. + public string Name { get; } +} diff --git a/VpSharp.Building/src/LightEffect.cs b/VpSharp.Building/src/LightEffect.cs new file mode 100644 index 0000000..ae243fd --- /dev/null +++ b/VpSharp.Building/src/LightEffect.cs @@ -0,0 +1,45 @@ +using System.ComponentModel; + +namespace VpSharp.Building; + +/// +/// An enumeration of light effects that can be used on the lightfx property on the light command. +/// +public enum LightEffect +{ + /// + /// No light effect. + /// + [Description("No light effect.")] None, + + /// + /// Blink light effect. + /// + [Description("Blink light effect.")] Blink, + + /// + /// Fade in light effect. + /// + [Description("Fade in light effect.")] FadeIn, + + /// + /// Fade out light effect. + /// + [Description("Fade out light effect.")] + FadeOut, + + /// + /// Fire light effect. + /// + [Description("Fire light effect.")] Fire, + + /// + /// Pulse light effect. + /// + [Description("Pulse light effect.")] Pulse, + + /// + /// Rainbow light effect. + /// + [Description("Rainbow light effect.")] Rainbow +} diff --git a/VpSharp.Building/src/LightType.cs b/VpSharp.Building/src/LightType.cs new file mode 100644 index 0000000..b4302ca --- /dev/null +++ b/VpSharp.Building/src/LightType.cs @@ -0,0 +1,19 @@ +using System.ComponentModel; + +namespace VpSharp.Building; + +/// +/// An enumeration of light types to use on the light command. +/// +public enum LightType +{ + /// + /// Point light. + /// + [Description("Point light.")] Point, + + /// + /// Spotlight. + /// + [Description("Spotlight.")] Spot +} diff --git a/VpSharp.Building/src/ParameterAttribute.cs b/VpSharp.Building/src/ParameterAttribute.cs new file mode 100644 index 0000000..b05b79b --- /dev/null +++ b/VpSharp.Building/src/ParameterAttribute.cs @@ -0,0 +1,29 @@ +namespace VpSharp.Building; + +/// +/// Defines the order of a parameter. +/// +[AttributeUsage(AttributeTargets.Property)] +public sealed class ParameterAttribute : Attribute +{ + /// + /// Initializes a new instance of the class. + /// + /// The parameter order. + public ParameterAttribute(int order) + { + Order = order; + } + + /// + /// Gets or sets a value indicating whether this parameter is optional. + /// + /// if the parameter is optional; otherwise, . + public bool IsOptional { get; set; } + + /// + /// Gets the order of this parameter. + /// + /// The parameter order. + public int Order { get; } +} diff --git a/VpSharp.Building/src/PropertyAttribute.cs b/VpSharp.Building/src/PropertyAttribute.cs new file mode 100644 index 0000000..9a38fec --- /dev/null +++ b/VpSharp.Building/src/PropertyAttribute.cs @@ -0,0 +1,35 @@ +namespace VpSharp.Building; + +/// +/// Defines a property. +/// +[AttributeUsage(AttributeTargets.Property)] +public sealed class PropertyAttribute : Attribute +{ + /// + /// Initializes a new instance of the class. + /// + /// The name of the property. + /// is . + /// is empty, or consists only of whitespace. + public PropertyAttribute(string propertyName) + { + if (propertyName is null) + { + throw new ArgumentNullException(nameof(propertyName)); + } + + if (string.IsNullOrWhiteSpace(propertyName)) + { + throw new ArgumentException("Property name cannot be empty", nameof(propertyName)); + } + + PropertyName = propertyName; + } + + /// + /// Gets the property name. + /// + /// The property name. + public string PropertyName { get; } +} diff --git a/VpSharp.Building/src/Serialization/ActionSerializer.Read.cs b/VpSharp.Building/src/Serialization/ActionSerializer.Read.cs new file mode 100644 index 0000000..fcb16c7 --- /dev/null +++ b/VpSharp.Building/src/Serialization/ActionSerializer.Read.cs @@ -0,0 +1,491 @@ +using System.Diagnostics.CodeAnalysis; +using System.Reflection; +using Cysharp.Text; +using X10D.Reflection; + +namespace VpSharp.Building.Serialization; + +public static partial class ActionSerializer +{ + /// + /// Deserializes a single value using the appropriate for the type. + /// + /// The value to deserialize. + /// Options to customize the behaviour of deserialization. + /// The type of the value to return. + /// The deserialized value. + public static T? DeserializeValue(string? value, ActionSerializerOptions? options) + { + if (value is null) + { + return default; + } + + options ??= new ActionSerializerOptions(); + + ValueConverter? converter = null; + + Type typeToConvert = typeof(T); + if (typeToConvert.GetCustomAttribute(true) is { } attribute) + { + converter = Activator.CreateInstance(attribute.ConverterType) as ValueConverter; + } + + converter ??= FindConverter(typeToConvert, options); + + if (converter is not null) + { + using var reader = new StringReader(value); + return (T?)converter.Read(reader, typeToConvert, options); + } + + return default; + } + + /// + /// Deserializes a single value using the appropriate for the type. + /// + /// The type of the value to return. + /// The value to deserialize. + /// Options to customize the behaviour of deserialization. + /// The deserialized value. + public static object? DeserializeValue(Type type, string? value, ActionSerializerOptions? options) + { + if (value is null) + { + return default; + } + + options ??= new ActionSerializerOptions(); + + ValueConverter? converter = null; + + if (type.GetCustomAttribute(true) is { } attribute) + { + converter = Activator.CreateInstance(attribute.ConverterType) as ValueConverter; + } + + converter ??= FindConverter(type, options); + + if (converter is not null) + { + using var reader = new StringReader(value); + return converter.Read(reader, type, options); + } + + return default; + } + + /// + /// Deserializes the specified Virtual Paradise action string to a . + /// + /// The Virtual Paradise action string. + /// Options to customize the behaviour of deserialization. + /// The deserialized action. + /// is . + public static VirtualParadiseAction Deserialize(string action, ActionSerializerOptions? options = null) + { + if (action is null) + { + throw new ArgumentNullException(nameof(action)); + } + + using var stream = new MemoryStream(); + Span bytes = stackalloc byte[Utf8NoBom.GetByteCount(action)]; + Utf8NoBom.GetBytes(action, bytes); + stream.Write(bytes); + + stream.Position = 0; + return Deserialize(stream, options); + } + + /// + /// Deserializes a stream containing a UTF-8 encoded Virtual Paradise action string to a + /// . + /// + /// The stream from which the action will be read. + /// Options to customize the behaviour of deserialization. + /// The deserialized action. + /// is . + /// does not support reading. + public static VirtualParadiseAction Deserialize(Stream utf8ActionStream, ActionSerializerOptions? options = null) + { + if (utf8ActionStream is null) + { + throw new ArgumentNullException(nameof(utf8ActionStream)); + } + + if (!utf8ActionStream.CanRead) + { + throw new NotSupportedException("Stream does not support reading"); + } + + using var reader = new StreamReader(utf8ActionStream, Utf8NoBom); + VirtualParadiseAction action = Deserialize(reader, options); + return action; + } + + /// + /// Deserializes a to a . + /// + /// The reader. + /// Options to customize the behaviour of deserialization. + /// The deserialized action. + public static VirtualParadiseAction Deserialize(TextReader reader, ActionSerializerOptions? options = null) + { + options ??= new ActionSerializerOptions(); + + Utf16ValueStringBuilder buffer = ZString.CreateStringBuilder(); + var builder = new VirtualParadiseActionBuilder(); + var rawTriggers = new List(); + + GetSyntaxGroup(reader, rawTriggers, TriggerSeparatorChar, ref buffer); + + foreach (string rawTrigger in rawTriggers) + { + using var triggerReader = new StringReader(rawTrigger); + string triggerName = ReadToken(triggerReader); + + TryCreateTrigger(triggerName, out Trigger trigger, options); + builder.AddTrigger(trigger); + + var rawCommands = new List(); + GetSyntaxGroup(triggerReader, rawCommands, CommandSeparatorChar, ref buffer); + + foreach (string rawCommand in rawCommands) + { + using var commandReader = new StringReader(rawCommand); + string commandName = ReadToken(commandReader); + + TryCreateCommand(commandName, out Command? command, options); + DeserializeCommand(commandReader, ref command, options); + + if (command is not null) + { + builder.AddCommand(command); + } + } + } + + buffer.Dispose(); + return builder.Build(); + } + + internal static void DeserializeCommand(TextReader reader, + ref Command? command, + ActionSerializerOptions options, + bool useDefinedConverter = true) + { + Type type = command!.GetType(); + if (useDefinedConverter && TryDefinedConverter(reader, options, type, out Command? result)) + { + command = result; + return; + } + + var arguments = new List(); + + while (true) + { + string token = ReadToken(reader); + if (string.IsNullOrWhiteSpace(token)) + { + break; + } + + arguments.Add(token); + } + + ParseArguments(command, options, arguments); + } + + private static void ParseArguments(Command command, + ActionSerializerOptions options, + List arguments) + { + var type = command.GetType(); + PropertyInfo[] members = type.GetProperties(BindingFlags.Public | BindingFlags.Instance); + PropertyInfo[] properties = members.Where(m => m.HasCustomAttribute()).ToArray(); + PropertyInfo[] flags = members.Where(m => m.HasCustomAttribute()).ToArray(); + PropertyInfo[] parameters = members + .Where(m => m.HasCustomAttribute()) + .OrderBy(m => m.GetCustomAttribute()!.Order) + .ToArray(); + + ExtractFlags(arguments, flags, command); + ExtractProperties(arguments, properties, command, options); + + int parameterIndex = 0; + for (var index = 0; index < arguments.Count; index++) + { + string argument = arguments[index]; + PropertyInfo parameter = parameters[parameterIndex]; + object? value = ConvertValue(parameter, argument, options); + parameter.SetValue(command, value); + parameterIndex++; + } + } + + private static void ExtractFlags(List arguments, PropertyInfo[] flags, Command command) + { + foreach (PropertyInfo member in flags) + { + var attribute = member.GetCustomAttribute()!; + for (var index = 0; index < arguments.Count; index++) + { + string argument = arguments[index]; + if (!string.Equals(argument, attribute.Name, StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + member.SetValue(command, true); + arguments.RemoveAt(index); + break; + } + } + } + + private static void ExtractProperties(List arguments, + PropertyInfo[] flags, + Command command, + ActionSerializerOptions options) + { + foreach (PropertyInfo member in flags) + { + var attribute = member.GetCustomAttribute()!; + for (var index = 0; index < arguments.Count; index++) + { + string argument = arguments[index]; + if (!IsProperty(argument, out int equalsIndex)) + { + continue; + } + + string propertyName = argument[..equalsIndex]; + if (!string.Equals(propertyName, attribute.PropertyName, StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + string value = argument[(equalsIndex + 1)..]; + member.SetValue(command, ConvertValue(member, value, options)); + arguments.RemoveAt(index); + break; + } + } + } + + private static bool IsProperty(string argument, out int index) + { + var insideString = false; + + for (index = 0; index < argument.Length; index++) + { + char current = argument[index]; + switch (current) + { + case '"': + insideString = !insideString; + break; + + case '=' when !insideString: + return true; + } + } + + index = -1; + return false; + } + + private static object? ConvertValue(PropertyInfo parameter, string token, ActionSerializerOptions options) + { + ValueConverter? converter = null; + + if (parameter.GetCustomAttribute() is { } attribute) + { + converter = Activator.CreateInstance(attribute.ConverterType) as ValueConverter; + } + + converter ??= FindConverter(parameter.PropertyType, options); + + if (converter is not null) + { + using var reader = new StringReader(token); + return converter.Read(reader, parameter.PropertyType, options); + } + + return null; + } + + private static ValueConverter? FindConverter(Type type, ActionSerializerOptions options) + { + ValueConverter? converter = null; + foreach (Type converterType in options.ValueConverters) + { + converter = Activator.CreateInstance(converterType) as ValueConverter; + if (converter is not null && converter.CanConvert(type)) + { + break; + } + + converter = null; + } + + return converter; + } + + private static bool TryDefinedConverter(TextReader reader, + ActionSerializerOptions options, + Type type, + [NotNullWhen(true)] out Command? command) + { + command = null; + + var attribute = type.GetCustomAttribute(); + if (attribute is null) + { + return false; + } + + ICollection commandConverters = options.CommandConverters; + if (type.GetCustomAttribute(true) is { } commandConverterAttribute) + { + commandConverters = [commandConverterAttribute.ConverterType, ..commandConverters]; + } + + foreach (Type converterType in commandConverters) + { + if (Activator.CreateInstance(converterType) is not CommandConverter converter || !converter.CanConvert(type)) + { + continue; + } + + command = converter.Read(reader, type, options); + return command is not null; + } + + return false; + } + + private static void GetSyntaxGroup(TextReader reader, List group, char delimiter, ref Utf16ValueStringBuilder buffer) + { + bool insideString = false; + + while (true) + { + char current = (char)reader.Read(); + switch (current) + { + case '"': + insideString = !insideString; + goto default; + + case var _ when current == delimiter: + group.Add(buffer.ToString()); + buffer.Clear(); + break; + + default: + buffer.Append(current); + break; + } + + if (reader.Peek() == -1) + { + break; + } + } + + string remainder = buffer.ToString(); + if (!string.IsNullOrWhiteSpace(remainder)) + { + group.Add(remainder); + } + + buffer.Clear(); + } + + private static string ReadToken(TextReader reader) + { + using Utf16ValueStringBuilder buffer = ZString.CreateStringBuilder(); + var insideString = false; + var reachedEnd = false; + + while (!reachedEnd) + { + int value = reader.Peek(); + if (value == -1) + { + break; + } + + char current = (char)value; + + switch (current) + { + case '"': + insideString = !insideString; + goto default; + + case CommandSeparatorChar: + case TriggerSeparatorChar: + reachedEnd = true; + break; + + case var _ when char.IsWhiteSpace(current) && !insideString: + reachedEnd = true; + goto default; + + default: + buffer.Append(current); + reader.Read(); // consume char + break; + } + } + + return buffer.ToString().Trim(); + } + + private static void TryCreateTrigger(string name, out Trigger trigger, ActionSerializerOptions options) + { + foreach (Type type in options.Triggers) + { + if (type.GetCustomAttribute() is not { } attribute) + { + continue; + } + + if (!string.Equals(name, attribute.TriggerName, StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + trigger = (Trigger)Activator.CreateInstance(type)!; + return; + } + + trigger = new UnknownTrigger(name); + } + + private static void TryCreateCommand(string name, out Command command, ActionSerializerOptions options) + { + foreach (Type commandType in options.Commands) + { + if (commandType.GetCustomAttribute() is not { } attribute) + { + continue; + } + + if (!string.Equals(name, attribute.CommandName, StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + command = (Command)Activator.CreateInstance(commandType)!; + return; + } + + command = new UnknownCommand(name); + } +} diff --git a/VpSharp.Building/src/Serialization/ActionSerializer.Write.cs b/VpSharp.Building/src/Serialization/ActionSerializer.Write.cs new file mode 100644 index 0000000..23d8178 --- /dev/null +++ b/VpSharp.Building/src/Serialization/ActionSerializer.Write.cs @@ -0,0 +1,367 @@ +using System.Reflection; + +namespace VpSharp.Building.Serialization; + +/// +/// Provides methods for serializing and deserializing Virtual Paradise actions. +/// +public static partial class ActionSerializer +{ + /// + /// Serializes the specified action to a Virtual Paradise action string. + /// + /// The action to serialize. + /// Options to customize the behaviour of serialization. + /// The serialized action string. + /// is . + public static string Serialize(VirtualParadiseAction action, ActionSerializerOptions? options = null) + { + if (action is null) + { + throw new ArgumentNullException(nameof(action)); + } + + using var stream = new MemoryStream(); + Serialize(stream, action, options); + + byte[] bytes = stream.ToArray(); + Span chars = stackalloc char[Utf8NoBom.GetCharCount(bytes)]; + Utf8NoBom.GetChars(bytes, chars); + return chars.ToString(); + } + + /// + /// Serializes the specified action to a Virtual Paradise action string. + /// + /// The stream to which the action will be written. + /// The action to serialize. + /// Options to customize the behaviour of serialization. + /// The serialized action string. + /// + /// is . + /// -or- + /// is . + /// + /// does not support writing. + public static void Serialize(Stream utf8ActionStream, VirtualParadiseAction action, ActionSerializerOptions? options = null) + { + if (utf8ActionStream is null) + { + throw new ArgumentNullException(nameof(utf8ActionStream)); + } + + if (action is null) + { + throw new ArgumentNullException(nameof(action)); + } + + if (!utf8ActionStream.CanWrite) + { + throw new NotSupportedException("Stream does not support writing"); + } + + options ??= new ActionSerializerOptions(); + using var writer = new Utf8ActionWriter(utf8ActionStream, options); + Serialize(writer, action, options); + } + + /// + /// Serializes the specified action to a Virtual Paradise action string. + /// + /// The writer to which the action will be written. + /// The action to serialize. + /// Options to customize the behaviour of serialization. + /// + /// + /// is . + /// -or- + /// is . + /// + public static void Serialize(Utf8ActionWriter writer, VirtualParadiseAction action, ActionSerializerOptions? options = null) + { + if (writer is null) + { + throw new ArgumentNullException(nameof(writer)); + } + + if (action is null) + { + throw new ArgumentNullException(nameof(action)); + } + + options ??= new ActionSerializerOptions(); + IReadOnlyList triggers = action.Triggers; + + if (triggers.Count == 0) + { + return; + } + + int triggerCount = triggers.Count; + for (var index = 0; index < triggerCount; index++) + { + var trigger = triggers[index]; + SerializeTrigger(writer, trigger, options); + + if (index < triggerCount - 1) + { + writer.Write(TriggerSeparatorChar); + writer.Write(' '); + } + } + + writer.Flush(); + } + + private static void SerializeTrigger(Utf8ActionWriter writer, Trigger trigger, ActionSerializerOptions options) + { + Type type = trigger.GetType(); + var attribute = type.GetCustomAttribute(); + if (attribute is null) + { + return; + } + + writer.Write(attribute.TriggerName); + IReadOnlyList commands = trigger.Commands; + + if (commands.Count == 0) + { + return; + } + + int commandCount = commands.Count; + for (var index = 0; index < commandCount; index++) + { + writer.Write(' '); + + var command = commands[index]; + SerializeCommand(writer, command, options); + + if (index < commandCount - 1) + { + writer.Write(CommandSeparatorChar); + } + } + } + + internal static void SerializeCommand(Utf8ActionWriter writer, + Command command, + ActionSerializerOptions options, + bool useDefinedConverter = true) + { + Type type = command.GetType(); + var attribute = type.GetCustomAttribute(); + if (attribute is null) + { + return; + } + + if (useDefinedConverter) + { + writer.Write(attribute.CommandName); + + ICollection commandConverters = options.CommandConverters; + if (type.GetCustomAttribute() is { } commandConverterAttribute) + { + commandConverters = [commandConverterAttribute.ConverterType, ..commandConverters]; + } + + foreach (Type converterType in commandConverters) + { + if (Activator.CreateInstance(converterType) is not CommandConverter converter || !converter.CanConvert(type)) + { + continue; + } + + converter.Write(writer, type, command, options); + return; + } + } + + PropertyInfo[] members = type.GetProperties(BindingFlags.Public | BindingFlags.Instance); + + SerializeParameters(writer, command, members, options); + SerializeFlags(writer, command, members); + SerializeProperties(writer, command, members, options); + } + + private static void SerializeFlags(Utf8ActionWriter writer, Command command, PropertyInfo[] members) + { + var actual = new List<(PropertyInfo Member, FlagAttribute Attribute)>(); + PopulateFlags(members, actual); + + int memberCount = actual.Count; + if (memberCount == 0) + { + return; + } + + var defaultInstance = (Command)Activator.CreateInstance(command.GetType())!; + + for (var index = 0; index < memberCount; index++) + { + (PropertyInfo member, FlagAttribute attribute) = actual[index]; + + object? value = member.GetValue(command); + object? defaultValue = member.GetValue(defaultInstance); + if (value is null || (value.Equals(defaultValue) && attribute.IsOptional)) + { + continue; + } + + writer.Write(' '); + writer.Write(attribute.Name); + } + } + + private static void SerializeParameters(Utf8ActionWriter writer, + Command command, + PropertyInfo[] members, + ActionSerializerOptions options) + { + var actual = new List<(PropertyInfo Member, ParameterAttribute Attribute)>(); + PopulateParameters(members, actual); + + int memberCount = actual.Count; + if (memberCount == 0) + { + return; + } + + actual.Sort((x, y) => x.Attribute.Order.CompareTo(y.Attribute.Order)); + var defaultInstance = (Command)Activator.CreateInstance(command.GetType())!; + + ICollection valueConverters = options.ValueConverters; + for (var index = 0; index < memberCount; index++) + { + (PropertyInfo member, ParameterAttribute attribute) = actual[index]; + + object? value = member.GetValue(command); + object? defaultValue = member.GetValue(defaultInstance); + if (value is null || value.Equals(defaultValue) && attribute.IsOptional) + { + continue; + } + + using var stream = new MemoryStream(); + using var buffer = new Utf8ActionWriter(stream, options); + + if (SerializeValue(buffer, command, member, options, valueConverters)) + { + writer.Write(' '); + + stream.Position = 0; + writer.Write(stream); + } + } + } + + private static void PopulateFlags(PropertyInfo[] members, List<(PropertyInfo, FlagAttribute)> actual) + { + foreach (PropertyInfo member in members) + { + var attribute = member.GetCustomAttribute(); + if (attribute is not null) + { + actual.Add((member, attribute)); + } + } + } + + private static void PopulateParameters(PropertyInfo[] members, List<(PropertyInfo, ParameterAttribute)> actual) + { + foreach (PropertyInfo member in members) + { + var attribute = member.GetCustomAttribute(); + if (attribute is not null) + { + actual.Add((member, attribute)); + } + } + } + + private static void SerializeProperties(Utf8ActionWriter writer, + Command command, + PropertyInfo[] members, + ActionSerializerOptions options) + { + ICollection valueConverters = options.ValueConverters; + var defaultInstance = (Command)Activator.CreateInstance(command.GetType())!; + + foreach (PropertyInfo member in members) + { + var attribute = member.GetCustomAttribute(); + if (attribute is null) + { + continue; + } + + object? value = member.GetValue(command); + if (value is null) + { + continue; + } + + object? defaultValue = member.GetValue(defaultInstance); + if (value.Equals(defaultValue)) + { + continue; + } + + using var stream = new MemoryStream(); + using var buffer = new Utf8ActionWriter(stream, options); + + if (SerializeValue(buffer, command, member, options, valueConverters)) + { + writer.Write(' '); + + writer.Write(attribute.PropertyName); + writer.Write('='); + + stream.Position = 0; + writer.Write(stream); + } + } + } + + private static bool SerializeValue(Utf8ActionWriter writer, + Command command, + PropertyInfo member, + ActionSerializerOptions options, + ICollection valueConverters) + { + object? value = member.GetValue(command); + if (value is null) + { + return false; + } + + Type type = member.PropertyType; + object?[]? args = null; + + if (member.GetCustomAttribute() is { } memberAttribute) + { + valueConverters = [memberAttribute.ConverterType, ..valueConverters]; + args = memberAttribute.Args; + } + else if (member.PropertyType.GetCustomAttribute() is { } typeAttribute) + { + valueConverters = [typeAttribute.ConverterType, ..valueConverters]; + args = typeAttribute.Args; + } + + foreach (Type converterType in valueConverters) + { + if (Activator.CreateInstance(converterType, args) is not ValueConverter converter || !converter.CanConvert(type)) + { + continue; + } + + converter.Write(writer, type, value, options); + return true; + } + + return false; + } +} diff --git a/VpSharp.Building/src/Serialization/ActionSerializer.cs b/VpSharp.Building/src/Serialization/ActionSerializer.cs new file mode 100644 index 0000000..95196a9 --- /dev/null +++ b/VpSharp.Building/src/Serialization/ActionSerializer.cs @@ -0,0 +1,39 @@ +using System.Text; + +namespace VpSharp.Building.Serialization; + +/// +/// Provides methods for serializing and deserializing Virtual Paradise actions. +/// +public static partial class ActionSerializer +{ + internal const char CommandSeparatorChar = ','; + internal const char TriggerSeparatorChar = ';'; + internal const char StringChar = '"'; + + private static readonly Encoding Utf8NoBom = new UTF8Encoding(false); + + /// + /// Returns a value indicating whether the specified input constitutes a Virtual Paradise boolean. + /// + /// The input to check. + /// if the input constitutes a valid boolean; otherwise, . + public static bool IsBooleanKeyword(string input) + { + if (string.Equals(input, "yes", StringComparison.OrdinalIgnoreCase) || + string.Equals(input, "on", StringComparison.OrdinalIgnoreCase) || + input == "1") + { + return true; + } + + if (string.Equals(input, "no", StringComparison.OrdinalIgnoreCase) || + string.Equals(input, "off", StringComparison.OrdinalIgnoreCase) || + input == "0") + { + return true; + } + + return false; + } +} diff --git a/VpSharp.Building/src/Serialization/CommandConverter.Generic.cs b/VpSharp.Building/src/Serialization/CommandConverter.Generic.cs new file mode 100644 index 0000000..9c4c7eb --- /dev/null +++ b/VpSharp.Building/src/Serialization/CommandConverter.Generic.cs @@ -0,0 +1,56 @@ +namespace VpSharp.Building.Serialization; + +/// +/// Represents a class which can convert a command. +/// +/// The type of the command to convert. +public abstract class CommandConverter : CommandConverter + where TCommand : Command +{ + /// + /// Initializes a new instance of the class. + /// + protected internal CommandConverter() + { + IsValueType = typeof(TCommand).IsValueType; + } + + /// + public sealed override Type? Type { get; } = typeof(TCommand); + + /// + public override bool CanConvert(Type typeToConvert) + { + return typeToConvert == typeof(TCommand); + } + + /// + /// Reads the command to from specified reader. + /// + /// The reader. + /// Options which customize the serialization options. + public abstract TCommand? Read(TextReader reader, ActionSerializerOptions options); + + /// + public override Command? Read(TextReader reader, Type typeToConvert, ActionSerializerOptions options) + { + return CanConvert(typeToConvert) ? Read(reader, options) : null; + } + + /// + /// Writes the command to the specified writer. + /// + /// The writer. + /// The command. + /// Options which customize the serialization options. + public abstract void Write(Utf8ActionWriter writer, TCommand? value, ActionSerializerOptions options); + + /// + public override void Write(Utf8ActionWriter writer, Type typeToConvert, Command? value, ActionSerializerOptions options) + { + if (CanConvert(typeToConvert) && value is TCommand actual) + { + Write(writer, actual, options); + } + } +} diff --git a/VpSharp.Building/src/Serialization/CommandConverter.cs b/VpSharp.Building/src/Serialization/CommandConverter.cs new file mode 100644 index 0000000..22174c6 --- /dev/null +++ b/VpSharp.Building/src/Serialization/CommandConverter.cs @@ -0,0 +1,132 @@ +namespace VpSharp.Building.Serialization; + +/// +/// Represents a class which can convert a command. +/// +public abstract class CommandConverter +{ + /// + /// Initializes a new instance of the class. + /// + protected CommandConverter() + { + IsInternalConverter = GetType().Assembly == typeof(CommandConverter).Assembly; + } + + /// + /// Gets the type being converted by the current converter instance. + /// + /// The command type. + public abstract Type? Type { get; } + + internal bool IsInternalConverter { get; init; } + + internal bool IsValueType { get; init; } + + /// + /// When overridden in a derived class, determines whether the converter instance can convert the specified object type. + /// + /// + /// The type of the object to check whether it can be converted by this converter instance. + /// + /// + /// if the instance can convert the specified object type; otherwise, . + /// + public abstract bool CanConvert(Type typeToConvert); + + /// + /// Reads the command to from specified reader. + /// + /// The reader. + /// The type of the command being converted. + /// Options which customize the serialization options. + /// The deserialized command, or if deserialization failed. + public abstract Command? Read(TextReader reader, Type typeToConvert, ActionSerializerOptions options); + + /// + /// Writes the command to the specified writer. + /// + /// The writer. + /// The type of the command being converted. + /// The value. + /// Options which customize the serialization options. + public abstract void Write(Utf8ActionWriter writer, Type typeToConvert, Command? value, ActionSerializerOptions options); + + /// + /// Reads the command to from specified reader using the default deserializer. + /// + /// The reader. + /// The command being converted. + /// Options which customize the serialization options. + protected void DefaultRead(TextReader reader, ref T command, ActionSerializerOptions options) + where T : Command + { + if (reader is null) + { + throw new ArgumentNullException(nameof(reader)); + } + + if (command is null) + { + throw new ArgumentNullException(nameof(command)); + } + + if (options is null) + { + throw new ArgumentNullException(nameof(options)); + } + + var result = (T?)DefaultRead(reader, command.GetType(), options); + if (result is not null) + { + command = result; + } + } + + /// + /// Reads the command to from specified reader using the default deserializer. + /// + /// The reader. + /// The type of the command being converted. + /// Options which customize the serialization options. + /// The deserialized command, or if deserialization failed. + protected Command? DefaultRead(TextReader reader, Type typeToConvert, ActionSerializerOptions options) + { + if (reader is null) + { + throw new ArgumentNullException(nameof(reader)); + } + + if (typeToConvert is null) + { + throw new ArgumentNullException(nameof(typeToConvert)); + } + + if (options is null) + { + throw new ArgumentNullException(nameof(options)); + } + + if (Activator.CreateInstance(typeToConvert) is not Command command) + { + return null; + } + + ActionSerializer.DeserializeCommand(reader, ref command!, options, false); + return command; + } + + /// + /// Writes the command to the specified writer using the default command converter. + /// + /// The writer. + /// The value. + /// Options which customize the serialization options. + protected void DefaultWrite(Utf8ActionWriter writer, Command? value, ActionSerializerOptions options) + { + if (value is not null) + { + ActionSerializer.SerializeCommand(writer, value, options, false); + } + } +} diff --git a/VpSharp.Building/src/Serialization/CommandConverterAttribute.Generic.cs b/VpSharp.Building/src/Serialization/CommandConverterAttribute.Generic.cs new file mode 100644 index 0000000..53b26f7 --- /dev/null +++ b/VpSharp.Building/src/Serialization/CommandConverterAttribute.Generic.cs @@ -0,0 +1,16 @@ +namespace VpSharp.Building.Serialization; + +/// +/// Defines the converter to use for this command. +/// +[AttributeUsage(AttributeTargets.Class)] +public sealed class CommandConverterAttribute : CommandConverterAttribute + where T : CommandConverter +{ + /// + /// Initializes a new instance of the class. + /// + public CommandConverterAttribute() : base(typeof(T)) + { + } +} diff --git a/VpSharp.Building/src/Serialization/CommandConverterAttribute.cs b/VpSharp.Building/src/Serialization/CommandConverterAttribute.cs new file mode 100644 index 0000000..eecbaa3 --- /dev/null +++ b/VpSharp.Building/src/Serialization/CommandConverterAttribute.cs @@ -0,0 +1,37 @@ +namespace VpSharp.Building.Serialization; + +/// +/// Defines the converter to use for this command. +/// +[AttributeUsage(AttributeTargets.Class)] +public class CommandConverterAttribute : Attribute +{ + /// + /// Initializes a new instance of the class. + /// + /// The type of the converter to use. + /// is . + /// + /// does not inherit . + /// + public CommandConverterAttribute(Type converterType) + { + if (converterType is null) + { + throw new ArgumentNullException(nameof(converterType)); + } + + if (!converterType.IsSubclassOf(typeof(CommandConverter))) + { + throw new ArgumentException($"Type does not inherit {typeof(CommandConverter)}"); + } + + ConverterType = converterType; + } + + /// + /// Gets the type of the converter to use. + /// + /// The type of the converter. + public Type ConverterType { get; } +} diff --git a/VpSharp.Building/src/Serialization/TokenType.cs b/VpSharp.Building/src/Serialization/TokenType.cs new file mode 100644 index 0000000..754b71b --- /dev/null +++ b/VpSharp.Building/src/Serialization/TokenType.cs @@ -0,0 +1,57 @@ +namespace VpSharp.Building.Serialization; + +/// +/// An enumeration of token types. +/// +public enum TokenType +{ + /// + /// No token type. + /// + None, + + /// + /// The token is a standard identifier or argument. + /// + Text, + + /// + /// The token is a trigger name. + /// + TriggerName, + + /// + /// The token is a command name. + /// + CommandName, + + /// + /// The token is a number. + /// + Number, + + /// + /// The token is a boolean. + /// + Boolean, + + /// + /// The token is an escaped string. + /// + String, + + /// + /// The token marks the end of a command. + /// + EndCommand, + + /// + /// The token marks the end of a trigger. + /// + EndTrigger, + + /// + /// The token is the end of the file. + /// + EndOfFile +} diff --git a/VpSharp.Building/src/Serialization/Utf8ActionWriter.cs b/VpSharp.Building/src/Serialization/Utf8ActionWriter.cs new file mode 100644 index 0000000..ae0f24f --- /dev/null +++ b/VpSharp.Building/src/Serialization/Utf8ActionWriter.cs @@ -0,0 +1,116 @@ +using System.Text; + +namespace VpSharp.Building.Serialization; + +/// +/// Represents a text writer which writes Virtual Paradise action string components. +/// +public sealed class Utf8ActionWriter : TextWriter +{ + private readonly Stream _stream; + private readonly ActionSerializerOptions _options; + + /// + /// Initializes a new instance of the class. + /// + /// The stream. + /// Options to customize the serialization behaviour. + public Utf8ActionWriter(Stream stream, ActionSerializerOptions? options) + { + _stream = stream; + _options = options ?? new ActionSerializerOptions(); + } + + /// + public override Encoding Encoding { get; } = Encoding.UTF8; + + /// + protected override void Dispose(bool disposing) + { + _stream.Dispose(); + base.Dispose(disposing); + } + + /// + public override void Write(char value) + { + Span span = [value]; + int byteCount = Encoding.GetByteCount(span); + + Span bytes = stackalloc byte[byteCount]; + Encoding.GetBytes(span, bytes); + _stream.Write(bytes); + } + + /// + /// Copies the contents of the specified stream to the current writer. The stream will be read from the current position, + /// and consumed in full. + /// + /// The stream whose contents to copy. + /// is . + /// does not support reading. + public void Write(Stream stream) + { + if (stream is null) + { + throw new ArgumentNullException(nameof(stream)); + } + + if (!stream.CanRead) + { + throw new NotSupportedException("Stream does not support reading."); + } + + stream.CopyTo(_stream); + } + + /// + /// Writes a property in the format name=value to the current writer. + /// + /// The property name. + /// The property value. + /// The type of . + /// is . + /// is empty or consists of only whitespace. + public void WriteProperty(string propertyName, TValue propertyValue) + { + if (propertyName is null) + { + throw new ArgumentNullException(nameof(propertyName)); + } + + if (string.IsNullOrWhiteSpace(propertyName)) + { + throw new ArgumentException("Property name cannot be empty.", nameof(propertyName)); + } + + Write(' '); + Write(propertyName); + Write('='); + WriteValue(propertyValue); + } + + /// + /// Writes a convertible value to the current writer. + /// + /// The value to write. + /// The type of . + public void WriteValue(TValue value) + { + foreach (Type converterType in _options.ValueConverters) + { + if (Activator.CreateInstance(converterType) is not ValueConverter valueConverter) + { + continue; + } + + if (!valueConverter.CanConvert(typeof(TValue))) + { + continue; + } + + valueConverter.Write(this, typeof(TValue), value, _options); + return; + } + } +} diff --git a/VpSharp.Building/src/Serialization/ValueConverter.Generic.cs b/VpSharp.Building/src/Serialization/ValueConverter.Generic.cs new file mode 100644 index 0000000..755a3c2 --- /dev/null +++ b/VpSharp.Building/src/Serialization/ValueConverter.Generic.cs @@ -0,0 +1,55 @@ +namespace VpSharp.Building.Serialization; + +/// +/// Represents a class which can convert a value. +/// +/// The type of the value to convert. +public abstract class ValueConverter : ValueConverter +{ + /// + /// Initializes a new instance of the class. + /// + protected internal ValueConverter() + { + IsValueType = typeof(TValue).IsValueType; + } + + /// + public sealed override Type? Type { get; } = typeof(TValue); + + /// + public override bool CanConvert(Type typeToConvert) + { + return typeToConvert == typeof(TValue); + } + + /// + /// Reads the value to from specified reader. + /// + /// The reader. + /// Options which customize the serialization options. + public abstract TValue? Read(TextReader reader, ActionSerializerOptions options); + + /// + public override object? Read(TextReader reader, Type typeToConvert, ActionSerializerOptions options) + { + return CanConvert(typeToConvert) ? Read(reader, options) : null; + } + + /// + /// Writes the value to the specified writer. + /// + /// The writer. + /// The value. + /// Options which customize the serialization options. + public abstract void Write(Utf8ActionWriter writer, TValue? value, ActionSerializerOptions options); + + /// + public override void Write(Utf8ActionWriter writer, Type typeToConvert, object? value, ActionSerializerOptions options) + { + if (CanConvert(typeToConvert) && value is TValue actual) + { + Write(writer, actual, options); + } + } +} diff --git a/VpSharp.Building/src/Serialization/ValueConverter.cs b/VpSharp.Building/src/Serialization/ValueConverter.cs new file mode 100644 index 0000000..e8af964 --- /dev/null +++ b/VpSharp.Building/src/Serialization/ValueConverter.cs @@ -0,0 +1,53 @@ +namespace VpSharp.Building.Serialization; + +/// +/// Represents a class which can convert a value. +/// +public abstract class ValueConverter +{ + /// + /// Initializes a new instance of the class. + /// + protected ValueConverter() + { + IsInternalConverter = GetType().Assembly == typeof(ValueConverter).Assembly; + } + + /// + /// Gets the type being converted by the current converter instance. + /// + /// The command type. + public abstract Type? Type { get; } + + internal bool IsInternalConverter { get; init; } + + internal bool IsValueType { get; init; } + + /// + /// When overridden in a derived class, determines whether the converter instance can convert the specified object type. + /// + /// + /// The type of the object to check whether it can be converted by this converter instance. + /// + /// + /// if the instance can convert the specified object type; otherwise, . + /// + public abstract bool CanConvert(Type typeToConvert); + + /// + /// Reads the value to from specified reader. + /// + /// The reader. + /// The type of the value being converted. + /// Options which customize the serialization options. + public abstract object? Read(TextReader reader, Type typeToConvert, ActionSerializerOptions options); + + /// + /// Writes the value to the specified writer. + /// + /// The writer. + /// The type of the value being converted. + /// The value. + /// Options which customize the serialization options. + public abstract void Write(Utf8ActionWriter writer, Type typeToConvert, object? value, ActionSerializerOptions options); +} diff --git a/VpSharp.Building/src/Serialization/ValueConverterAttribute.Generic.cs b/VpSharp.Building/src/Serialization/ValueConverterAttribute.Generic.cs new file mode 100644 index 0000000..22f6f82 --- /dev/null +++ b/VpSharp.Building/src/Serialization/ValueConverterAttribute.Generic.cs @@ -0,0 +1,17 @@ +namespace VpSharp.Building.Serialization; + +/// +/// Defines the value converter to use for this property. +/// +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Property)] +public sealed class ValueConverterAttribute : ValueConverterAttribute + where TValueConverter : ValueConverter +{ + /// + /// Initializes a new instance of the class. + /// + /// Arguments to pass to the constructor. + public ValueConverterAttribute(params object?[]? args) : base(typeof(TValueConverter), args) + { + } +} diff --git a/VpSharp.Building/src/Serialization/ValueConverterAttribute.cs b/VpSharp.Building/src/Serialization/ValueConverterAttribute.cs new file mode 100644 index 0000000..53501d6 --- /dev/null +++ b/VpSharp.Building/src/Serialization/ValueConverterAttribute.cs @@ -0,0 +1,45 @@ +namespace VpSharp.Building.Serialization; + +/// +/// Defines the value converter to use for this property. +/// +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Property)] +public class ValueConverterAttribute : Attribute +{ + /// + /// Initializes a new instance of the class. + /// + /// The type of the converter to use. + /// Arguments to pass to the constructor. + /// is . + /// + /// does not inherit . + /// + public ValueConverterAttribute(Type converterType, params object?[]? args) + { + if (converterType is null) + { + throw new ArgumentNullException(nameof(converterType)); + } + + if (!converterType.IsSubclassOf(typeof(ValueConverter))) + { + throw new ArgumentException($"Type does not inherit {typeof(ValueConverter)}"); + } + + ConverterType = converterType; + Args = args; + } + + /// + /// Gets the arguments to be passed to the converter's constructor. + /// + /// An array of objects to be passed to the constructor. + public object?[]? Args { get; } + + /// + /// Gets the type of the converter to use. + /// + /// The type of the converter. + public Type ConverterType { get; } +} diff --git a/VpSharp.Building/src/TextAlignment.cs b/VpSharp.Building/src/TextAlignment.cs new file mode 100644 index 0000000..b5dc6d7 --- /dev/null +++ b/VpSharp.Building/src/TextAlignment.cs @@ -0,0 +1,25 @@ +using System.ComponentModel; + +namespace VpSharp.Building; + +/// +/// An enumeration of text alignments. +/// +public enum TextAlignment +{ + /// + /// Center text alignment. + /// + [Description("Center text alignment.")] + Center, + + /// + /// Left text alignment. + /// + [Description("Left text alignment.")] Left, + + /// + /// Right text alignment. + /// + [Description("Right text alignment.")] Right +} diff --git a/VpSharp.Building/src/Trigger.cs b/VpSharp.Building/src/Trigger.cs new file mode 100644 index 0000000..f125d22 --- /dev/null +++ b/VpSharp.Building/src/Trigger.cs @@ -0,0 +1,55 @@ +namespace VpSharp.Building; + +/// +/// Represents a trigger. +/// +public abstract class Trigger +{ + /// + /// Gets the commands in this trigger. + /// + /// A read-only view of the commands in this trigger. + public IReadOnlyList Commands { get; internal set; } = []; +} + +/// +/// Represents an unknown trigger. +/// +internal sealed class UnknownTrigger : Trigger +{ + /// + /// Initializes a new instance of the class. + /// + /// The name of the trigger. + public UnknownTrigger(string triggerName) + { + TriggerName = triggerName; + } + + /// + /// Gets the name of the trigger. + /// + /// The name of the trigger. + public string TriggerName { get; } +} + +/// +/// Represents an unknown command. +/// +internal sealed class UnknownCommand : Command +{ + /// + /// Initializes a new instance of the class. + /// + /// The name of the command. + public UnknownCommand(string commandName) + { + CommandName = commandName; + } + + /// + /// Gets the name of the command. + /// + /// The name of the command. + public string CommandName { get; } +} diff --git a/VpSharp.Building/src/TriggerAttribute.cs b/VpSharp.Building/src/TriggerAttribute.cs new file mode 100644 index 0000000..28651e7 --- /dev/null +++ b/VpSharp.Building/src/TriggerAttribute.cs @@ -0,0 +1,35 @@ +namespace VpSharp.Building; + +/// +/// Defines the name of a trigger. +/// +[AttributeUsage(AttributeTargets.Class)] +public sealed class TriggerAttribute : Attribute +{ + /// + /// Initializes a new instance of the class. + /// + /// The name of the trigger. + /// is . + /// is empty, or consists only of whitespace. + public TriggerAttribute(string triggerName) + { + if (triggerName is null) + { + throw new ArgumentNullException(nameof(triggerName)); + } + + if (string.IsNullOrWhiteSpace(triggerName)) + { + throw new ArgumentException("Trigger name cannot be empty", nameof(triggerName)); + } + + TriggerName = triggerName; + } + + /// + /// Gets the name of the trigger. + /// + /// The name of the trigger. + public string TriggerName { get; } +} diff --git a/VpSharp.Building/src/ValueConverters/BooleanValueConverter.cs b/VpSharp.Building/src/ValueConverters/BooleanValueConverter.cs new file mode 100644 index 0000000..534cbc8 --- /dev/null +++ b/VpSharp.Building/src/ValueConverters/BooleanValueConverter.cs @@ -0,0 +1,38 @@ +using VpSharp.Building.Extensions; +using VpSharp.Building.Serialization; + +namespace VpSharp.Building.ValueConverters; + +/// +/// Represents a converter which can convert values of type . +/// +public sealed class BooleanValueConverter : ValueConverter +{ + /// + public override bool Read(TextReader reader, ActionSerializerOptions options) + { + string token = reader.ReadToSpace(); + return !(string.Equals(token, "0", StringComparison.OrdinalIgnoreCase) || + string.Equals(token, "off", StringComparison.OrdinalIgnoreCase) || + string.Equals(token, "no", StringComparison.OrdinalIgnoreCase)); + } + + /// + public override void Write(Utf8ActionWriter writer, bool value, ActionSerializerOptions options) + { + switch (options.BooleanMode) + { + case BooleanMode.OneZero: + writer.Write(value ? '1' : '0'); + break; + + case BooleanMode.OnOff: + writer.Write(value ? "on" : "off"); + break; + + case BooleanMode.YesNo: + writer.Write(value ? "yes" : "no"); + break; + } + } +} diff --git a/VpSharp.Building/src/ValueConverters/ByteValueConverter.cs b/VpSharp.Building/src/ValueConverters/ByteValueConverter.cs new file mode 100644 index 0000000..b061899 --- /dev/null +++ b/VpSharp.Building/src/ValueConverters/ByteValueConverter.cs @@ -0,0 +1,23 @@ +using VpSharp.Building.Extensions; +using VpSharp.Building.Serialization; + +namespace VpSharp.Building.ValueConverters; + +/// +/// Represents a converter which can convert values of type . +/// +public sealed class ByteValueConverter : ValueConverter +{ + /// + public override byte Read(TextReader reader, ActionSerializerOptions options) + { + string token = reader.ReadToSpace(); + return byte.Parse(token); + } + + /// + public override void Write(Utf8ActionWriter writer, byte value, ActionSerializerOptions options) + { + writer.Write(value); + } +} diff --git a/VpSharp.Building/src/ValueConverters/CharValueConverter.cs b/VpSharp.Building/src/ValueConverters/CharValueConverter.cs new file mode 100644 index 0000000..0a3364a --- /dev/null +++ b/VpSharp.Building/src/ValueConverters/CharValueConverter.cs @@ -0,0 +1,21 @@ +using VpSharp.Building.Serialization; + +namespace VpSharp.Building.ValueConverters; + +/// +/// Represents a converter which can convert values of type . +/// +public sealed class CharValueConverter : ValueConverter +{ + /// + public override char Read(TextReader reader, ActionSerializerOptions options) + { + return (char)reader.Read(); + } + + /// + public override void Write(Utf8ActionWriter writer, char value, ActionSerializerOptions options) + { + writer.Write(value); + } +} diff --git a/VpSharp.Building/src/ValueConverters/ColorValueConverter.cs b/VpSharp.Building/src/ValueConverters/ColorValueConverter.cs new file mode 100644 index 0000000..7c0d2e1 --- /dev/null +++ b/VpSharp.Building/src/ValueConverters/ColorValueConverter.cs @@ -0,0 +1,38 @@ +using System.Drawing; +using VpSharp.Building.Extensions; +using VpSharp.Building.Serialization; + +namespace VpSharp.Building.ValueConverters; + +/// +/// Represents a converter which can convert values of type . +/// +public sealed class ColorValueConverter : ValueConverter +{ + /// + public override Color Read(TextReader reader, ActionSerializerOptions options) + { + string token = reader.ReadToSpace(); + return VirtualParadiseColors.KnownColors.TryGetValue(token, out int color) + ? Color.FromArgb(color) + : ColorTranslator.FromHtml($"#{token}"); + } + + /// + public override void Write(Utf8ActionWriter writer, Color value, ActionSerializerOptions options) + { + int argb = value.ToArgb(); + if (VirtualParadiseColors.KnownColors.ContainsValue(argb)) + { + KeyValuePair pair = VirtualParadiseColors.KnownColors.First(c => c.Value == argb); + writer.Write(pair.Key); + return; + } + + Span chars = stackalloc char[6]; + value.R.TryFormat(chars[..2], out _, "X2"); + value.G.TryFormat(chars[2..4], out _, "X2"); + value.B.TryFormat(chars[4..], out _, "X2"); + writer.Write(chars); + } +} diff --git a/VpSharp.Building/src/ValueConverters/DecimalValueConverter.cs b/VpSharp.Building/src/ValueConverters/DecimalValueConverter.cs new file mode 100644 index 0000000..e85d80d --- /dev/null +++ b/VpSharp.Building/src/ValueConverters/DecimalValueConverter.cs @@ -0,0 +1,23 @@ +using VpSharp.Building.Extensions; +using VpSharp.Building.Serialization; + +namespace VpSharp.Building.ValueConverters; + +/// +/// Represents a converter which can convert values of type . +/// +public sealed class DecimalValueConverter : ValueConverter +{ + /// + public override decimal Read(TextReader reader, ActionSerializerOptions options) + { + string token = reader.ReadToSpace(); + return decimal.Parse(token); + } + + /// + public override void Write(Utf8ActionWriter writer, decimal value, ActionSerializerOptions options) + { + writer.Write(value); + } +} diff --git a/VpSharp.Building/src/ValueConverters/DoubleValueConverter.cs b/VpSharp.Building/src/ValueConverters/DoubleValueConverter.cs new file mode 100644 index 0000000..690c875 --- /dev/null +++ b/VpSharp.Building/src/ValueConverters/DoubleValueConverter.cs @@ -0,0 +1,23 @@ +using VpSharp.Building.Extensions; +using VpSharp.Building.Serialization; + +namespace VpSharp.Building.ValueConverters; + +/// +/// Represents a converter which can convert values of type . +/// +public sealed class DoubleValueConverter : ValueConverter +{ + /// + public override double Read(TextReader reader, ActionSerializerOptions options) + { + string token = reader.ReadToSpace(); + return double.Parse(token); + } + + /// + public override void Write(Utf8ActionWriter writer, double value, ActionSerializerOptions options) + { + writer.Write(value); + } +} diff --git a/VpSharp.Building/src/ValueConverters/Int16ValueConverter.cs b/VpSharp.Building/src/ValueConverters/Int16ValueConverter.cs new file mode 100644 index 0000000..0687d55 --- /dev/null +++ b/VpSharp.Building/src/ValueConverters/Int16ValueConverter.cs @@ -0,0 +1,23 @@ +using VpSharp.Building.Extensions; +using VpSharp.Building.Serialization; + +namespace VpSharp.Building.ValueConverters; + +/// +/// Represents a converter which can convert values of type . +/// +public sealed class Int16ValueConverter : ValueConverter +{ + /// + public override short Read(TextReader reader, ActionSerializerOptions options) + { + string token = reader.ReadToSpace(); + return short.Parse(token); + } + + /// + public override void Write(Utf8ActionWriter writer, short value, ActionSerializerOptions options) + { + writer.Write(value); + } +} diff --git a/VpSharp.Building/src/ValueConverters/Int32ValueConverter.cs b/VpSharp.Building/src/ValueConverters/Int32ValueConverter.cs new file mode 100644 index 0000000..fd6ad2d --- /dev/null +++ b/VpSharp.Building/src/ValueConverters/Int32ValueConverter.cs @@ -0,0 +1,23 @@ +using VpSharp.Building.Extensions; +using VpSharp.Building.Serialization; + +namespace VpSharp.Building.ValueConverters; + +/// +/// Represents a converter which can convert values of type . +/// +public sealed class Int32ValueConverter : ValueConverter +{ + /// + public override int Read(TextReader reader, ActionSerializerOptions options) + { + string token = reader.ReadToSpace(); + return int.Parse(token); + } + + /// + public override void Write(Utf8ActionWriter writer, int value, ActionSerializerOptions options) + { + writer.Write(value); + } +} diff --git a/VpSharp.Building/src/ValueConverters/Int64ValueConverter.cs b/VpSharp.Building/src/ValueConverters/Int64ValueConverter.cs new file mode 100644 index 0000000..ef47d63 --- /dev/null +++ b/VpSharp.Building/src/ValueConverters/Int64ValueConverter.cs @@ -0,0 +1,23 @@ +using VpSharp.Building.Extensions; +using VpSharp.Building.Serialization; + +namespace VpSharp.Building.ValueConverters; + +/// +/// Represents a converter which can convert values of type . +/// +public sealed class Int64ValueConverter : ValueConverter +{ + /// + public override long Read(TextReader reader, ActionSerializerOptions options) + { + string token = reader.ReadToSpace(); + return long.Parse(token); + } + + /// + public override void Write(Utf8ActionWriter writer, long value, ActionSerializerOptions options) + { + writer.Write(value); + } +} diff --git a/VpSharp.Building/src/ValueConverters/SByteValueConverter.cs b/VpSharp.Building/src/ValueConverters/SByteValueConverter.cs new file mode 100644 index 0000000..3fe7ae6 --- /dev/null +++ b/VpSharp.Building/src/ValueConverters/SByteValueConverter.cs @@ -0,0 +1,24 @@ +using VpSharp.Building.Extensions; +using VpSharp.Building.Serialization; + +namespace VpSharp.Building.ValueConverters; + +/// +/// Represents a converter which can convert values of type . +/// +[CLSCompliant(false)] +public sealed class SByteValueConverter : ValueConverter +{ + /// + public override sbyte Read(TextReader reader, ActionSerializerOptions options) + { + string token = reader.ReadToSpace(); + return sbyte.Parse(token); + } + + /// + public override void Write(Utf8ActionWriter writer, sbyte value, ActionSerializerOptions options) + { + writer.Write(value); + } +} diff --git a/VpSharp.Building/src/ValueConverters/SingleValueConverter.cs b/VpSharp.Building/src/ValueConverters/SingleValueConverter.cs new file mode 100644 index 0000000..194eb82 --- /dev/null +++ b/VpSharp.Building/src/ValueConverters/SingleValueConverter.cs @@ -0,0 +1,23 @@ +using VpSharp.Building.Extensions; +using VpSharp.Building.Serialization; + +namespace VpSharp.Building.ValueConverters; + +/// +/// Represents a converter which can convert values of type . +/// +public sealed class SingleValueConverter : ValueConverter +{ + /// + public override float Read(TextReader reader, ActionSerializerOptions options) + { + string token = reader.ReadToSpace(); + return float.Parse(token); + } + + /// + public override void Write(Utf8ActionWriter writer, float value, ActionSerializerOptions options) + { + writer.Write(value); + } +} diff --git a/VpSharp.Building/src/ValueConverters/StringValueConverter.cs b/VpSharp.Building/src/ValueConverters/StringValueConverter.cs new file mode 100644 index 0000000..62bec24 --- /dev/null +++ b/VpSharp.Building/src/ValueConverters/StringValueConverter.cs @@ -0,0 +1,77 @@ +using Cysharp.Text; +using VpSharp.Building.Extensions; +using VpSharp.Building.Serialization; + +namespace VpSharp.Building.ValueConverters; + +/// +/// Represents a converter which can convert values of type . +/// +public sealed class StringValueConverter : ValueConverter +{ + private readonly bool _escape; + + /// + /// Initializes a new instance of the class. + /// + public StringValueConverter() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// if the string should be escaped; otherwise, . + public StringValueConverter(bool escape) + { + _escape = escape; + } + + /// + public override string? Read(TextReader reader, ActionSerializerOptions options) + { + if (!_escape) + { + return reader.ReadToSpace(); + } + + using Utf16ValueStringBuilder builder = ZString.CreateStringBuilder(); + bool insideString = false; + + while (true) + { + int current = reader.Read(); + if (current == -1 || (!insideString && char.IsWhiteSpace((char)current))) + { + break; + } + + if (current == '"') + { + insideString = !insideString; + } + else + { + builder.Append((char)current); + } + } + + return builder.ToString(); + } + + /// + public override void Write(Utf8ActionWriter writer, string? value, ActionSerializerOptions options) + { + if (_escape) + { + writer.Write('"'); + } + + writer.Write(value); + + if (_escape) + { + writer.Write('"'); + } + } +} diff --git a/VpSharp.Building/src/ValueConverters/UInt16ValueConverter.cs b/VpSharp.Building/src/ValueConverters/UInt16ValueConverter.cs new file mode 100644 index 0000000..af6b7db --- /dev/null +++ b/VpSharp.Building/src/ValueConverters/UInt16ValueConverter.cs @@ -0,0 +1,24 @@ +using VpSharp.Building.Extensions; +using VpSharp.Building.Serialization; + +namespace VpSharp.Building.ValueConverters; + +/// +/// Represents a converter which can convert values of type . +/// +[CLSCompliant(false)] +public sealed class UInt16ValueConverter : ValueConverter +{ + /// + public override ushort Read(TextReader reader, ActionSerializerOptions options) + { + string token = reader.ReadToSpace(); + return ushort.Parse(token); + } + + /// + public override void Write(Utf8ActionWriter writer, ushort value, ActionSerializerOptions options) + { + writer.Write(value); + } +} diff --git a/VpSharp.Building/src/ValueConverters/UInt32ValueConverter.cs b/VpSharp.Building/src/ValueConverters/UInt32ValueConverter.cs new file mode 100644 index 0000000..605e482 --- /dev/null +++ b/VpSharp.Building/src/ValueConverters/UInt32ValueConverter.cs @@ -0,0 +1,24 @@ +using VpSharp.Building.Extensions; +using VpSharp.Building.Serialization; + +namespace VpSharp.Building.ValueConverters; + +/// +/// Represents a converter which can convert values of type . +/// +[CLSCompliant(false)] +public sealed class UInt32ValueConverter : ValueConverter +{ + /// + public override uint Read(TextReader reader, ActionSerializerOptions options) + { + string token = reader.ReadToSpace(); + return uint.Parse(token); + } + + /// + public override void Write(Utf8ActionWriter writer, uint value, ActionSerializerOptions options) + { + writer.Write(value); + } +} diff --git a/VpSharp.Building/src/ValueConverters/UInt64ValueConverter.cs b/VpSharp.Building/src/ValueConverters/UInt64ValueConverter.cs new file mode 100644 index 0000000..c8c969d --- /dev/null +++ b/VpSharp.Building/src/ValueConverters/UInt64ValueConverter.cs @@ -0,0 +1,24 @@ +using VpSharp.Building.Extensions; +using VpSharp.Building.Serialization; + +namespace VpSharp.Building.ValueConverters; + +/// +/// Represents a converter which can convert values of type . +/// +[CLSCompliant(false)] +public sealed class UInt64ValueConverter : ValueConverter +{ + /// + public override ulong Read(TextReader reader, ActionSerializerOptions options) + { + string token = reader.ReadToSpace(); + return ulong.Parse(token); + } + + /// + public override void Write(Utf8ActionWriter writer, ulong value, ActionSerializerOptions options) + { + writer.Write(value); + } +} diff --git a/VpSharp.Building/src/ValueConverters/Vector3ValueConverter.cs b/VpSharp.Building/src/ValueConverters/Vector3ValueConverter.cs new file mode 100644 index 0000000..3d41def --- /dev/null +++ b/VpSharp.Building/src/ValueConverters/Vector3ValueConverter.cs @@ -0,0 +1,45 @@ +using System.Drawing; +using System.Numerics; +using VpSharp.Building.Extensions; +using VpSharp.Building.Serialization; + +namespace VpSharp.Building.ValueConverters; + +/// +/// Represents a converter which can convert values of type . +/// +public sealed class Vector3ValueConverter : ValueConverter +{ + /// + public override Vector3 Read(TextReader reader, ActionSerializerOptions options) + { + float.TryParse(reader.ReadToSpace(), out float x); + char peek = (char)reader.Peek(); + + if (peek is '-' or '+' || char.IsDigit(peek)) + { + float.TryParse(reader.ReadToSpace(), out float y); + float.TryParse(reader.ReadToSpace(), out float z); + return new Vector3(x, y, z); + } + + return new Vector3(0, x, 0); + } + + /// + public override void Write(Utf8ActionWriter writer, Vector3 value, ActionSerializerOptions options) + { + if (value is { X: 0, Z: 0 }) + { + writer.Write(value.Y); + } + else + { + writer.Write(value.X); + writer.Write(' '); + writer.Write(value.Y); + writer.Write(' '); + writer.Write(value.Z); + } + } +} diff --git a/VpSharp.Building/src/VirtualParadiseAction.cs b/VpSharp.Building/src/VirtualParadiseAction.cs new file mode 100644 index 0000000..ae75976 --- /dev/null +++ b/VpSharp.Building/src/VirtualParadiseAction.cs @@ -0,0 +1,171 @@ +using System.Diagnostics.CodeAnalysis; +using System.Reflection; + +namespace VpSharp.Building; + +/// +/// Represents a Virtual Paradise object action string. +/// +public sealed class VirtualParadiseAction +{ + /// + /// An empty action. + /// + public static readonly VirtualParadiseAction Empty = new(); + + private readonly Dictionary> _triggers = []; + + /// + /// Gets the triggers in this action. + /// + /// A read-only view of the triggers in this action. + public IReadOnlyList Triggers + { + get => _triggers.Values.SelectMany(v => v).ToArray(); + internal set + { + _triggers.Clear(); + + foreach (Trigger trigger in value) + { + var attribute = trigger.GetType().GetCustomAttribute(); + if (attribute is null) + { + continue; + } + + if (!_triggers.TryGetValue(attribute.TriggerName, out List? triggers)) + { + triggers = new List(); + _triggers[attribute.TriggerName] = triggers; + } + + triggers.Add(trigger); + } + } + } + + /// + /// Gets the first trigger of the specified type. + /// + /// The type of the trigger to return. + /// The matching trigger, or if no matching trigger was found. + /// is . + /// does not inherit . + public Trigger? GetTrigger(Type triggerType) + { + if (triggerType is null) + { + throw new ArgumentNullException(nameof(triggerType)); + } + + if (!triggerType.IsSubclassOf(typeof(Trigger))) + { + throw new ArgumentException($"Type does not inherit {typeof(Trigger)}", nameof(triggerType)); + } + + foreach (Trigger trigger in Triggers) + { + if (trigger.GetType() == triggerType) + { + return trigger; + } + } + + return null; + } + + /// + /// Gets all triggers of the specified type. + /// + /// The type of the trigger to return. + /// An ordered collection of all the matching triggers. + /// is . + /// does not inherit . + public IReadOnlyList GetTriggers(Type triggerType) + { + if (triggerType is null) + { + throw new ArgumentNullException(nameof(triggerType)); + } + + if (!triggerType.IsSubclassOf(typeof(Trigger))) + { + throw new ArgumentException($"Type does not inherit {typeof(Trigger)}", nameof(triggerType)); + } + + var triggers = new List(); + + foreach (Trigger trigger in Triggers) + { + if (trigger.GetType() == triggerType) + { + triggers.Add(trigger); + } + } + + return triggers.AsReadOnly(); + } + + /// + /// Gets the first trigger of the specified type. + /// + /// The type of the trigger to return. + /// The matching trigger, or if no matching trigger was found. + public T? GetTrigger() where T : Trigger + { + return (T?)GetTrigger(typeof(T)); + } + + /// + /// Gets all triggers of the specified type. + /// + /// The type of the trigger to return. + /// An ordered collection of all the matching triggers. + public IReadOnlyList GetTriggers() where T : Trigger + { + return (IReadOnlyList)GetTriggers(typeof(T)); + } + + /// + /// Attempts to find the first trigger of the specified type. + /// + /// The type of the trigger to return. + /// + /// When this method returns, contains the first matching trigger whose type is equal to , + /// if such a trigger exists; otherwise, . + /// + /// if a matching trigger was found; otherwise, + /// + /// + public bool TryGetTrigger(Type triggerType, [NotNullWhen(true)] out Trigger? trigger) + { + if (triggerType is null) + { + throw new ArgumentNullException(nameof(triggerType)); + } + + if (!triggerType.IsSubclassOf(typeof(Trigger))) + { + throw new ArgumentException($"Type does not inherit {typeof(Trigger)}", nameof(triggerType)); + } + + trigger = GetTrigger(triggerType); + return trigger is not null; + } + + /// + /// Attempts to find the first trigger of the specified type. + /// + /// + /// When this method returns, contains the first matching trigger whose type is equal to , + /// if such a trigger exists; otherwise, . + /// + /// The type of the trigger to return. + /// if a matching trigger was found; otherwise, + public bool TryGetTrigger([NotNullWhen(true)] out T? trigger) where T : Trigger + { + trigger = GetTrigger(); + return trigger is not null; + } +} diff --git a/VpSharp.Building/src/VirtualParadiseActionBuilder.cs b/VpSharp.Building/src/VirtualParadiseActionBuilder.cs new file mode 100644 index 0000000..73f0ded --- /dev/null +++ b/VpSharp.Building/src/VirtualParadiseActionBuilder.cs @@ -0,0 +1,119 @@ +namespace VpSharp.Building; + +/// +/// Represents a mutable . +/// +public sealed class VirtualParadiseActionBuilder +{ + private Trigger? _currentTrigger; + + /// + /// Initializes a new instance of the class. + /// + public VirtualParadiseActionBuilder() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The existing action. + /// is . + public VirtualParadiseActionBuilder(VirtualParadiseAction action) + { + if (action is null) + { + throw new ArgumentNullException(nameof(action)); + } + + Triggers = [..action.Triggers]; + Commands = Triggers.SelectMany(t => t.Commands).ToList(); + } + + /// + /// Gets or sets the list of commands in the current action. + /// + /// The list of commands. + public IList Commands { get; set; } = []; + + /// + /// Gets or sets the list of triggers in this action. + /// + /// The list of triggers. + public IList Triggers { get; set; } = []; + + /// + /// Adds a command to this action. + /// + /// The command to add. + /// The current instance. + /// is . + public VirtualParadiseActionBuilder AddCommand(Command command) + { + if (command is null) + { + throw new ArgumentNullException(nameof(command)); + } + + // ReSharper disable once NullCoalescingConditionIsAlwaysNotNullAccordingToAPIContract + Commands ??= []; + Commands.Add(command); + return this; + } + + /// + /// Adds a trigger to this action. + /// + /// The type of the trigger to add. + /// The current instance. + public VirtualParadiseActionBuilder AddTrigger() + where TTrigger : Trigger + { + if (_currentTrigger is not null) + { + _currentTrigger.Commands = Commands.ToArray(); + } + + var trigger = Activator.CreateInstance(); + return AddTrigger(trigger); + } + + /// + /// Adds a trigger to this action. + /// + /// The trigger to add. + /// The current instance. + /// is . + public VirtualParadiseActionBuilder AddTrigger(Trigger trigger) + { + if (trigger is null) + { + throw new ArgumentNullException(nameof(trigger)); + } + + if (_currentTrigger is not null) + { + _currentTrigger.Commands = Commands.ToArray(); + } + + // ReSharper disable once NullCoalescingConditionIsAlwaysNotNullAccordingToAPIContract + Triggers ??= []; + Triggers.Add(trigger); + _currentTrigger = trigger; + return this; + } + + /// + /// Builds the action. + /// + /// The newly-built action. + public VirtualParadiseAction Build() + { + if (_currentTrigger is not null) + { + _currentTrigger.Commands = Commands.ToArray(); + } + + return new VirtualParadiseAction { Triggers = [..Triggers] }; + } +} diff --git a/VpSharp.Building/src/VirtualParadiseColors.cs b/VpSharp.Building/src/VirtualParadiseColors.cs new file mode 100644 index 0000000..d5d899b --- /dev/null +++ b/VpSharp.Building/src/VirtualParadiseColors.cs @@ -0,0 +1,308 @@ +using System.Drawing; + +namespace VpSharp.Building; + +/// +/// Offers predefined named colors supported by Virtual Paradise. +/// +public static class VirtualParadiseColors +{ + internal static readonly Dictionary KnownColors = new(StringComparer.OrdinalIgnoreCase) + { + ["aquamarine"] = Aquamarine.ToArgb(), + ["black"] = Black.ToArgb(), + ["blue"] = Blue.ToArgb(), + ["brass"] = Brass.ToArgb(), + ["bronze"] = Bronze.ToArgb(), + ["copper"] = Copper.ToArgb(), + ["cyan"] = Cyan.ToArgb(), + ["darkgrey"] = DarkGrey.ToArgb(), + ["forestgreen"] = ForestGreen.ToArgb(), + ["gold"] = Gold.ToArgb(), + ["green"] = Green.ToArgb(), + ["grey"] = Grey.ToArgb(), + ["lightgrey"] = LightGrey.ToArgb(), + ["magenta"] = Magenta.ToArgb(), + ["maroon"] = Maroon.ToArgb(), + ["navyBlue"] = NavyBlue.ToArgb(), + ["orange"] = Orange.ToArgb(), + ["orangered"] = OrangeRed.ToArgb(), + ["orchid"] = Orchid.ToArgb(), + ["pink"] = Pink.ToArgb(), + ["red"] = Red.ToArgb(), + ["salmon"] = Salmon.ToArgb(), + ["scarlet"] = Scarlet.ToArgb(), + ["silver"] = Silver.ToArgb(), + ["skyblue"] = SkyBlue.ToArgb(), + ["tan"] = Tan.ToArgb(), + ["teal"] = Teal.ToArgb(), + ["turquoise"] = Turquoise.ToArgb(), + ["violet"] = Violet.ToArgb(), + ["white"] = White.ToArgb(), + ["yellow"] = Yellow.ToArgb() + }; + + /// + /// The color #70DB93. + /// + public static Color Aquamarine + { + get => Color.FromArgb(0x70, 0xDB, 0x93); + } + + /// + /// The color #000000. + /// + public static Color Black + { + get => Color.FromArgb(0x00, 0x00, 0x00); + } + + /// + /// The color #0000FF. + /// + public static Color Blue + { + get => Color.FromArgb(0x00, 0x00, 0xFF); + } + + /// + /// The color #B5A642. + /// + public static Color Brass + { + get => Color.FromArgb(0xB5, 0xA6, 0x42); + } + + /// + /// The color #8C7853. + /// + public static Color Bronze + { + get => Color.FromArgb(0x8C, 0x78, 0x53); + } + + /// + /// The color #B87333. + /// + public static Color Copper + { + get => Color.FromArgb(0xB8, 0x73, 0x33); + } + + /// + /// The color #00FFFF. + /// + public static Color Cyan + { + get => Color.FromArgb(0x00, 0xFF, 0xFF); + } + + /// + /// The color #303030. + /// + public static Color DarkGrey + { + get => Color.FromArgb(0x30, 0x30, 0x30); + } + + /// + /// The color #0000C0. + /// + public static Color DefaultSignBackColor + { + get => Color.FromArgb(0x00, 0x00, 0xC0); + } + + /// + /// The color #238E23. + /// + public static Color ForestGreen + { + get => Color.FromArgb(0x23, 0x8E, 0x23); + } + + /// + /// The color #CD7F32. + /// + public static Color Gold + { + get => Color.FromArgb(0xCD, 0x7F, 0x32); + } + + /// + /// The color #00FF00. + /// + public static Color Green + { + get => Color.FromArgb(0x00, 0xFF, 0x00); + } + + /// + /// The color #707070. + /// + public static Color Grey + { + get => Color.FromArgb(0x70, 0x70, 0x70); + } + + /// + /// The color #C0C0C0. + /// + public static Color LightGrey + { + get => Color.FromArgb(0xC0, 0xC0, 0xC0); + } + + /// + /// The color #FF00FF. + /// + public static Color Magenta + { + get => Color.FromArgb(0xFF, 0x00, 0xFF); + } + + /// + /// The color #8E236B. + /// + public static Color Maroon + { + get => Color.FromArgb(0x8E, 0x23, 0x6B); + } + + /// + /// The color #23238E. + /// + public static Color NavyBlue + { + get => Color.FromArgb(0x23, 0x23, 0x8E); + } + + /// + /// The color #FF7F00. + /// + public static Color Orange + { + get => Color.FromArgb(0xFF, 0x7F, 0x00); + } + + /// + /// The color #FF2400. + /// + public static Color OrangeRed + { + get => Color.FromArgb(0xFF, 0x24, 0x00); + } + + /// + /// The color #DB70DB. + /// + public static Color Orchid + { + get => Color.FromArgb(0xDB, 0x70, 0xDB); + } + + /// + /// The color #FF6EC7. + /// + public static Color Pink + { + get => Color.FromArgb(0xFF, 0x6E, 0xC7); + } + + /// + /// The color #FF0000. + /// + public static Color Red + { + get => Color.FromArgb(0xFF, 0x00, 0x00); + } + + /// + /// The color #6F4242. + /// + public static Color Salmon + { + get => Color.FromArgb(0x6F, 0x42, 0x42); + } + + /// + /// The color #8C1717. + /// + public static Color Scarlet + { + get => Color.FromArgb(0x8C, 0x17, 0x17); + } + + /// + /// The color #E6E8FA. + /// + public static Color Silver + { + get => Color.FromArgb(0xE6, 0xE8, 0xFA); + } + + /// + /// The color #3299CC. + /// + public static Color SkyBlue + { + get => Color.FromArgb(0x32, 0x99, 0xCC); + } + + /// + /// The color #DB9370. + /// + public static Color Tan + { + get => Color.FromArgb(0xDB, 0x93, 0x70); + } + + /// + /// The color #007070. + /// + public static Color Teal + { + get => Color.FromArgb(0x00, 0x70, 0x70); + } + + /// + /// Transparent. + /// + public static Color Transparent + { + get => Color.FromArgb(0x00, 0x00, 0x00, 0x00); + } + + /// + /// The color #ADEAEA. + /// + public static Color Turquoise + { + get => Color.FromArgb(0xAD, 0xEA, 0xEA); + } + + /// + /// The color #4F2F4F. + /// + public static Color Violet + { + get => Color.FromArgb(0x4F, 0x2F, 0x4F); + } + + /// + /// The color #FFFFFF. + /// + public static Color White + { + get => Color.FromArgb(0xFF, 0xFF, 0xFF); + } + + /// + /// The color #FFFF00. + /// + public static Color Yellow + { + get => Color.FromArgb(0xFF, 0xFF, 0x00); + } +} diff --git a/VpSharp.sln b/VpSharp.sln index 2deae48..c0d0924 100644 --- a/VpSharp.sln +++ b/VpSharp.sln @@ -32,6 +32,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Workflows", "Workflows", "{ .github\workflows\release.yml = .github\workflows\release.yml EndProjectSection EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VpSharp.Building", "VpSharp.Building\VpSharp.Building.csproj", "{75A37D40-3C54-4F0F-AA49-38DA15FAC217}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VpSharp.Building.Tests", "VpSharp.Building.Tests\VpSharp.Building.Tests.csproj", "{5EE67C7B-E592-4F85-BD07-4E2B52C1E183}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -62,5 +66,13 @@ Global {EF3BBA1B-8E9B-49A7-87C0-BE7D99E86B8F}.Debug|Any CPU.Build.0 = Debug|Any CPU {EF3BBA1B-8E9B-49A7-87C0-BE7D99E86B8F}.Release|Any CPU.ActiveCfg = Release|Any CPU {EF3BBA1B-8E9B-49A7-87C0-BE7D99E86B8F}.Release|Any CPU.Build.0 = Release|Any CPU + {75A37D40-3C54-4F0F-AA49-38DA15FAC217}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {75A37D40-3C54-4F0F-AA49-38DA15FAC217}.Debug|Any CPU.Build.0 = Debug|Any CPU + {75A37D40-3C54-4F0F-AA49-38DA15FAC217}.Release|Any CPU.ActiveCfg = Release|Any CPU + {75A37D40-3C54-4F0F-AA49-38DA15FAC217}.Release|Any CPU.Build.0 = Release|Any CPU + {5EE67C7B-E592-4F85-BD07-4E2B52C1E183}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5EE67C7B-E592-4F85-BD07-4E2B52C1E183}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5EE67C7B-E592-4F85-BD07-4E2B52C1E183}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5EE67C7B-E592-4F85-BD07-4E2B52C1E183}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal