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