feat: add action (de)serialization

This commit is contained in:
Oliver Booth 2024-06-18 15:45:49 +01:00
parent 183261fedd
commit 59a9043d97
Signed by: oliverbooth
GPG Key ID: E60B570D1B7557B5
52 changed files with 3430 additions and 0 deletions

View File

@ -0,0 +1,30 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net6.0;net8.0</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
<NoWarn>CS1591</NoWarn>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.2"/>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.10.0"/>
<PackageReference Include="NUnit" Version="4.1.0"/>
<PackageReference Include="NUnit.Analyzers" Version="4.2.0"/>
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0"/>
</ItemGroup>
<ItemGroup>
<Using Include="NUnit.Framework"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\VpSharp.Building\VpSharp.Building.csproj"/>
</ItemGroup>
</Project>

View File

@ -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)));
});
}
}

View File

@ -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();
}
}

View File

@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net6.0;net8.0</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Optional" Version="4.0.0"/>
<PackageReference Include="X10D" Version="4.0.0"/>
<PackageReference Include="ZString" Version="2.6.0"/>
</ItemGroup>
</Project>

View File

@ -0,0 +1,96 @@
using VpSharp.Building.Serialization;
namespace VpSharp.Building;
/// <summary>
/// Defines options for serialization Virtual Paradise action strings.
/// </summary>
public sealed class ActionSerializerOptions
{
/// <summary>
/// Gets or initializes the boolean literal mode.
/// </summary>
/// <value>The boolean literal mode. Defaults to <see cref="BooleanMode.OnOff" />.</value>
public BooleanMode BooleanMode { get; init; } = BooleanMode.OnOff;
/// <summary>
/// Gets or initializes the custom command converters.
/// </summary>
/// <value>The custom command converters.</value>
public ICollection<Type> CustomCommandConverters { get; init; } = [];
/// <summary>
/// Gets or initializes the custom command types.
/// </summary>
/// <value>The custom command types.</value>
public ICollection<Type> CustomCommands { get; init; } = [];
/// <summary>
/// Gets or initializes the custom triggers types.
/// </summary>
/// <value>The custom triggers types.</value>
public ICollection<Type> CustomTriggers { get; init; } = [];
/// <summary>
/// Gets or initializes the custom value converters.
/// </summary>
/// <value>The custom value converters.</value>
public ICollection<Type> CustomValueConverters { get; init; } = [];
internal ICollection<Type> CommandConverters
{
get => [..CustomCommandConverters, ..GetInternalCommandConverters([])];
}
internal ICollection<Type> Commands
{
get => [..CustomCommands, ..GetInternalCommands([])];
}
internal ICollection<Type> Triggers
{
get => [..CustomTriggers, ..GetInternalTriggers([])];
}
internal ICollection<Type> ValueConverters
{
get => [..CustomValueConverters, ..GetInternalValueConverters([])];
}
private static Type[] GetInternalCommandConverters(IEnumerable<Type> except)
{
return typeof(CommandConverter).Assembly
.GetTypes()
.Except(except)
.Where(t => !t.IsAbstract && t.IsSubclassOf(typeof(CommandConverter)))
.ToArray();
}
private static Type[] GetInternalCommands(IEnumerable<Type> except)
{
return typeof(Command).Assembly
.GetTypes()
.Except(except)
.Where(t => !t.IsAbstract && t.IsSubclassOf(typeof(Command)))
.ToArray();
}
private static Type[] GetInternalTriggers(IEnumerable<Type> except)
{
return typeof(Trigger).Assembly
.GetTypes()
.Except(except)
.Where(t => !t.IsAbstract && t.IsSubclassOf(typeof(Trigger)))
.ToArray();
}
private static Type[] GetInternalValueConverters(IEnumerable<Type> except)
{
return typeof(ValueConverter).Assembly
.GetTypes()
.Except(except)
.Where(t => !t.IsAbstract && t.IsSubclassOf(typeof(ValueConverter)))
.ToArray();
}
}

View File

@ -0,0 +1,3 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("VpSharp.Building.Tests")]

View File

@ -0,0 +1,22 @@
namespace VpSharp.Building;
/// <summary>
/// An enumeration of boolean modes.
/// </summary>
public enum BooleanMode
{
/// <summary>
/// Booleans will be written with the literals <c>on</c> and <c>off</c>.
/// </summary>
OnOff,
/// <summary>
/// Booleans will be written with the literals <c>yes</c> and <c>no</c>.
/// </summary>
YesNo,
/// <summary>
/// Booleans will be written with the literals <c>1</c> and <c>0</c>.
/// </summary>
OneZero
}

View File

@ -0,0 +1,86 @@
using System.Reflection;
using VpSharp.Building.Serialization;
namespace VpSharp.Building;
/// <summary>
/// Represents a command.
/// </summary>
public abstract class Command
{
/// <summary>
/// Gets the flags for this command.
/// </summary>
/// <value>The flags.</value>
public IReadOnlyList<string> Flags { get; internal set; } = [];
/// <summary>
/// Gets the properties for this command.
/// </summary>
/// <value>The properties.</value>
public Dictionary<string, string> Properties { get; internal set; } = new(StringComparer.OrdinalIgnoreCase);
/// <summary>
/// Gets the raw arguments for this command.
/// </summary>
/// <value>The raw arguments.</value>
public IReadOnlyList<string> RawArguments { get; internal set; } = [];
/// <summary>
/// Gets the raw argument string for this command.
/// </summary>
/// <value>The raw argument string.</value>
public string RawArgumentString { get; internal set; } = string.Empty;
/// <summary>
/// Gets or sets the target of the command.
/// </summary>
/// <value>The name of the target object.</value>
/// <remarks>The target is defined by the <c>name</c> property.</remarks>
[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<PropertyAttribute>() 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;
}
}
}
}

View File

@ -0,0 +1,35 @@
namespace VpSharp.Building;
/// <summary>
/// Defines the name of a command.
/// </summary>
[AttributeUsage(AttributeTargets.Class)]
public sealed class CommandAttribute : Attribute
{
/// <summary>
/// Initializes a new instance of the <see cref="CommandAttribute" /> class.
/// </summary>
/// <param name="commandName">The name of the command.</param>
/// <exception cref="ArgumentNullException"><paramref name="commandName" /> is <see langword="null" />.</exception>
/// <exception cref="ArgumentException"><paramref name="commandName" /> is empty, or consists only of whitespace.</exception>
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;
}
/// <summary>
/// Gets the name of the command.
/// </summary>
/// <value>The name of the command.</value>
public string CommandName { get; }
}

View File

@ -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();
}
}

View File

@ -0,0 +1,195 @@
using System.Diagnostics.CodeAnalysis;
using VpSharp.Building.Triggers;
namespace VpSharp.Building.Extensions;
/// <summary>
/// Extension methods for <see cref="VirtualParadiseAction" />.
/// </summary>
public static class VirtualParadiseActionExtensions
{
/// <summary>
/// Returns the <c>activate</c> trigger.
/// </summary>
/// <param name="action">The action whose triggers to search.</param>
/// <returns>The <c>activate</c> trigger from this action, or <see langword="null" /> if no such trigger exists.</returns>
/// <exception cref="ArgumentNullException"><paramref name="action" /> is <see langword="null" />.</exception>
public static ActivateTrigger? Activate(this VirtualParadiseAction action)
{
if (action is null)
{
throw new ArgumentNullException(nameof(action));
}
return action.GetTrigger<ActivateTrigger>();
}
/// <summary>
/// Returns the <c>adone</c> trigger.
/// </summary>
/// <param name="action">The action whose triggers to search.</param>
/// <returns>The <c>adone</c> trigger from this action, or <see langword="null" /> if no such trigger exists.</returns>
/// <exception cref="ArgumentNullException"><paramref name="action" /> is <see langword="null" />.</exception>
public static ADoneTrigger? ADone(this VirtualParadiseAction action)
{
if (action is null)
{
throw new ArgumentNullException(nameof(action));
}
return action.GetTrigger<ADoneTrigger>();
}
/// <summary>
/// Returns the <c>bump</c> trigger.
/// </summary>
/// <param name="action">The action whose triggers to search.</param>
/// <returns>The <c>bump</c> trigger from this action, or <see langword="null" /> if no such trigger exists.</returns>
/// <exception cref="ArgumentNullException"><paramref name="action" /> is <see langword="null" />.</exception>
public static BumpTrigger? Bump(this VirtualParadiseAction action)
{
if (action is null)
{
throw new ArgumentNullException(nameof(action));
}
return action.GetTrigger<BumpTrigger>();
}
/// <summary>
/// Returns the <c>bumpend</c> trigger.
/// </summary>
/// <param name="action">The action whose triggers to search.</param>
/// <returns>The <c>bumpend</c> trigger from this action, or <see langword="null" /> if no such trigger exists.</returns>
/// <exception cref="ArgumentNullException"><paramref name="action" /> is <see langword="null" />.</exception>
public static BumpEndTrigger? BumpEnd(this VirtualParadiseAction action)
{
if (action is null)
{
throw new ArgumentNullException(nameof(action));
}
return action.GetTrigger<BumpEndTrigger>();
}
/// <summary>
/// Returns the <c>create</c> trigger.
/// </summary>
/// <param name="action">The action whose triggers to search.</param>
/// <returns>The <c>create</c> trigger from this action, or <see langword="null" /> if no such trigger exists.</returns>
/// <exception cref="ArgumentNullException"><paramref name="action" /> is <see langword="null" />.</exception>
public static CreateTrigger? Create(this VirtualParadiseAction action)
{
if (action is null)
{
throw new ArgumentNullException(nameof(action));
}
return action.GetTrigger<CreateTrigger>();
}
/// <summary>
/// Returns the <c>activate</c> trigger.
/// </summary>
/// <param name="action">The action whose triggers to search.</param>
/// <param name="trigger">
/// When this method returns, contains the <c>activate</c> trigger from this action, or <see langword="null" /> if no such
/// trigger exists.
/// </param>
/// <returns><see langword="true" /> if the trigger was found; otherwise, <see langword="false" />.</returns>
/// <exception cref="ArgumentNullException"><paramref name="action" /> is <see langword="null" />.</exception>
public static bool TryGetActivate(this VirtualParadiseAction action, [NotNullWhen(true)] out ActivateTrigger? trigger)
{
if (action is null)
{
throw new ArgumentNullException(nameof(action));
}
trigger = action.GetTrigger<ActivateTrigger>();
return trigger is not null;
}
/// <summary>
/// Returns the <c>adone</c> trigger.
/// </summary>
/// <param name="action">The action whose triggers to search.</param>
/// <param name="trigger">
/// When this method returns, contains the <c>adone</c> trigger from this action, or <see langword="null" /> if no such
/// trigger exists.
/// </param>
/// <returns><see langword="true" /> if the trigger was found; otherwise, <see langword="false" />.</returns>
/// <exception cref="ArgumentNullException"><paramref name="action" /> is <see langword="null" />.</exception>
public static bool TryGetADone(this VirtualParadiseAction action, [NotNullWhen(true)] out ADoneTrigger? trigger)
{
if (action is null)
{
throw new ArgumentNullException(nameof(action));
}
trigger = action.GetTrigger<ADoneTrigger>();
return trigger is not null;
}
/// <summary>
/// Returns the <c>bump</c> trigger.
/// </summary>
/// <param name="action">The action whose triggers to search.</param>
/// <param name="trigger">
/// When this method returns, contains the <c>bump</c> trigger from this action, or <see langword="null" /> if no such
/// trigger exists.
/// </param>
/// <returns><see langword="true" /> if the trigger was found; otherwise, <see langword="false" />.</returns>
/// <exception cref="ArgumentNullException"><paramref name="action" /> is <see langword="null" />.</exception>
public static bool TryGetBump(this VirtualParadiseAction action, [NotNullWhen(true)] out BumpTrigger? trigger)
{
if (action is null)
{
throw new ArgumentNullException(nameof(action));
}
trigger = action.GetTrigger<BumpTrigger>();
return trigger is not null;
}
/// <summary>
/// Returns the <c>bumpend</c> trigger.
/// </summary>
/// <param name="action">The action whose triggers to search.</param>
/// <param name="trigger">
/// When this method returns, contains the <c>bumpend</c> trigger from this action, or <see langword="null" /> if no such
/// trigger exists.
/// </param>
/// <returns><see langword="true" /> if the trigger was found; otherwise, <see langword="false" />.</returns>
/// <exception cref="ArgumentNullException"><paramref name="action" /> is <see langword="null" />.</exception>
public static bool TryGetBumpEnd(this VirtualParadiseAction action, [NotNullWhen(true)] out BumpEndTrigger? trigger)
{
if (action is null)
{
throw new ArgumentNullException(nameof(action));
}
trigger = action.GetTrigger<BumpEndTrigger>();
return trigger is not null;
}
/// <summary>
/// Returns the <c>create</c> trigger.
/// </summary>
/// <param name="action">The action whose triggers to search.</param>
/// <param name="trigger">
/// When this method returns, contains the <c>create</c> trigger from this action, or <see langword="null" /> if no such
/// trigger exists.
/// </param>
/// <returns><see langword="true" /> if the trigger was found; otherwise, <see langword="false" />.</returns>
/// <exception cref="ArgumentNullException"><paramref name="action" /> is <see langword="null" />.</exception>
public static bool TryGetCreate(this VirtualParadiseAction action, [NotNullWhen(true)] out CreateTrigger? trigger)
{
if (action is null)
{
throw new ArgumentNullException(nameof(action));
}
trigger = action.GetTrigger<CreateTrigger>();
return trigger is not null;
}
}

View File

@ -0,0 +1,46 @@
namespace VpSharp.Building;
/// <summary>
/// Defines a flag.
/// </summary>
public sealed class FlagAttribute : Attribute
{
/// <summary>
/// Initializes a new instance of the <see cref="FlagAttribute" /> class.
/// </summary>
/// <param name="name">The name of the flag.</param>
/// <exception cref="ArgumentNullException"><paramref name="name" /> is <see langword="null" />.</exception>
/// <exception cref="ArgumentException"><paramref name="name" /> is empty or consists of only whitespace.</exception>
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;
}
/// <summary>
/// Gets or sets the default value of the flag.
/// </summary>
/// <value>The default value.</value>
public bool DefaultValue { get; set; } = false;
/// <summary>
/// Gets or sets a value indicating whether this flag is optional.
/// </summary>
/// <value><see langword="true" /> if this flag is optional; otherwise, <see langword="false" />.</value>
public bool IsOptional { get; set; } = true;
/// <summary>
/// Gets the name of the flag.
/// </summary>
/// <value>The name of the flag.</value>
public string Name { get; }
}

View File

@ -0,0 +1,45 @@
using System.ComponentModel;
namespace VpSharp.Building;
/// <summary>
/// An enumeration of light effects that can be used on the <c>lightfx</c> property on the <c>light</c> command.
/// </summary>
public enum LightEffect
{
/// <summary>
/// No light effect.
/// </summary>
[Description("No light effect.")] None,
/// <summary>
/// Blink light effect.
/// </summary>
[Description("Blink light effect.")] Blink,
/// <summary>
/// Fade in light effect.
/// </summary>
[Description("Fade in light effect.")] FadeIn,
/// <summary>
/// Fade out light effect.
/// </summary>
[Description("Fade out light effect.")]
FadeOut,
/// <summary>
/// Fire light effect.
/// </summary>
[Description("Fire light effect.")] Fire,
/// <summary>
/// Pulse light effect.
/// </summary>
[Description("Pulse light effect.")] Pulse,
/// <summary>
/// Rainbow light effect.
/// </summary>
[Description("Rainbow light effect.")] Rainbow
}

View File

@ -0,0 +1,19 @@
using System.ComponentModel;
namespace VpSharp.Building;
/// <summary>
/// An enumeration of light types to use on the <c>light</c> command.
/// </summary>
public enum LightType
{
/// <summary>
/// Point light.
/// </summary>
[Description("Point light.")] Point,
/// <summary>
/// Spotlight.
/// </summary>
[Description("Spotlight.")] Spot
}

View File

@ -0,0 +1,29 @@
namespace VpSharp.Building;
/// <summary>
/// Defines the order of a parameter.
/// </summary>
[AttributeUsage(AttributeTargets.Property)]
public sealed class ParameterAttribute : Attribute
{
/// <summary>
/// Initializes a new instance of the <see cref="ParameterAttribute" /> class.
/// </summary>
/// <param name="order">The parameter order.</param>
public ParameterAttribute(int order)
{
Order = order;
}
/// <summary>
/// Gets or sets a value indicating whether this parameter is optional.
/// </summary>
/// <value><see langword="true" /> if the parameter is optional; otherwise, <see langword="false" />.</value>
public bool IsOptional { get; set; }
/// <summary>
/// Gets the order of this parameter.
/// </summary>
/// <value>The parameter order.</value>
public int Order { get; }
}

View File

@ -0,0 +1,35 @@
namespace VpSharp.Building;
/// <summary>
/// Defines a property.
/// </summary>
[AttributeUsage(AttributeTargets.Property)]
public sealed class PropertyAttribute : Attribute
{
/// <summary>
/// Initializes a new instance of the <see cref="PropertyAttribute" /> class.
/// </summary>
/// <param name="propertyName">The name of the property.</param>
/// <exception cref="ArgumentNullException"><paramref name="propertyName" /> is <see langword="null" />.</exception>
/// <exception cref="ArgumentException"><paramref name="propertyName" /> is empty, or consists only of whitespace.</exception>
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;
}
/// <summary>
/// Gets the property name.
/// </summary>
/// <value>The property name.</value>
public string PropertyName { get; }
}

View File

@ -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
{
/// <summary>
/// Deserializes a single value using the appropriate <see cref="ValueConverter" /> for the type.
/// </summary>
/// <param name="value">The value to deserialize.</param>
/// <param name="options">Options to customize the behaviour of deserialization.</param>
/// <typeparam name="T">The type of the value to return.</typeparam>
/// <returns>The deserialized value.</returns>
public static T? DeserializeValue<T>(string? value, ActionSerializerOptions? options)
{
if (value is null)
{
return default;
}
options ??= new ActionSerializerOptions();
ValueConverter? converter = null;
Type typeToConvert = typeof(T);
if (typeToConvert.GetCustomAttribute<ValueConverterAttribute>(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;
}
/// <summary>
/// Deserializes a single value using the appropriate <see cref="ValueConverter" /> for the type.
/// </summary>
/// <param name="type">The type of the value to return.</param>
/// <param name="value">The value to deserialize.</param>
/// <param name="options">Options to customize the behaviour of deserialization.</param>
/// <returns>The deserialized value.</returns>
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<ValueConverterAttribute>(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;
}
/// <summary>
/// Deserializes the specified Virtual Paradise action string to a <see cref="VirtualParadiseAction" />.
/// </summary>
/// <param name="action">The Virtual Paradise action string.</param>
/// <param name="options">Options to customize the behaviour of deserialization.</param>
/// <returns>The deserialized action.</returns>
/// <exception cref="ArgumentNullException"><paramref name="action" /> is <see langword="null" />.</exception>
public static VirtualParadiseAction Deserialize(string action, ActionSerializerOptions? options = null)
{
if (action is null)
{
throw new ArgumentNullException(nameof(action));
}
using var stream = new MemoryStream();
Span<byte> bytes = stackalloc byte[Utf8NoBom.GetByteCount(action)];
Utf8NoBom.GetBytes(action, bytes);
stream.Write(bytes);
stream.Position = 0;
return Deserialize(stream, options);
}
/// <summary>
/// Deserializes a stream containing a UTF-8 encoded Virtual Paradise action string to a
/// <see cref="VirtualParadiseAction" />.
/// </summary>
/// <param name="utf8ActionStream">The stream from which the action will be read.</param>
/// <param name="options">Options to customize the behaviour of deserialization.</param>
/// <returns>The deserialized action.</returns>
/// <exception cref="ArgumentNullException"><paramref name="utf8ActionStream" /> is <see langword="null" />.</exception>
/// <exception cref="NotSupportedException"><paramref name="utf8ActionStream" /> does not support reading.</exception>
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;
}
/// <summary>
/// Deserializes a <see cref="TextReader" /> to a <see cref="VirtualParadiseAction" />.
/// </summary>
/// <param name="reader">The reader.</param>
/// <param name="options">Options to customize the behaviour of deserialization.</param>
/// <returns>The deserialized action.</returns>
public static VirtualParadiseAction Deserialize(TextReader reader, ActionSerializerOptions? options = null)
{
options ??= new ActionSerializerOptions();
Utf16ValueStringBuilder buffer = ZString.CreateStringBuilder();
var builder = new VirtualParadiseActionBuilder();
var rawTriggers = new List<string>();
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<string>();
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<string>();
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<string> arguments)
{
var type = command.GetType();
PropertyInfo[] members = type.GetProperties(BindingFlags.Public | BindingFlags.Instance);
PropertyInfo[] properties = members.Where(m => m.HasCustomAttribute<PropertyAttribute>()).ToArray();
PropertyInfo[] flags = members.Where(m => m.HasCustomAttribute<FlagAttribute>()).ToArray();
PropertyInfo[] parameters = members
.Where(m => m.HasCustomAttribute<ParameterAttribute>())
.OrderBy(m => m.GetCustomAttribute<ParameterAttribute>()!.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<string> arguments, PropertyInfo[] flags, Command command)
{
foreach (PropertyInfo member in flags)
{
var attribute = member.GetCustomAttribute<FlagAttribute>()!;
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<string> arguments,
PropertyInfo[] flags,
Command command,
ActionSerializerOptions options)
{
foreach (PropertyInfo member in flags)
{
var attribute = member.GetCustomAttribute<PropertyAttribute>()!;
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<ValueConverterAttribute>() 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<CommandAttribute>();
if (attribute is null)
{
return false;
}
ICollection<Type> commandConverters = options.CommandConverters;
if (type.GetCustomAttribute<CommandConverterAttribute>(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<string> 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<TriggerAttribute>() 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<CommandAttribute>() is not { } attribute)
{
continue;
}
if (!string.Equals(name, attribute.CommandName, StringComparison.OrdinalIgnoreCase))
{
continue;
}
command = (Command)Activator.CreateInstance(commandType)!;
return;
}
command = new UnknownCommand(name);
}
}

View File

@ -0,0 +1,367 @@
using System.Reflection;
namespace VpSharp.Building.Serialization;
/// <summary>
/// Provides methods for serializing and deserializing Virtual Paradise actions.
/// </summary>
public static partial class ActionSerializer
{
/// <summary>
/// Serializes the specified action to a Virtual Paradise action string.
/// </summary>
/// <param name="action">The action to serialize.</param>
/// <param name="options">Options to customize the behaviour of serialization.</param>
/// <returns>The serialized action string.</returns>
/// <exception cref="ArgumentNullException"><paramref name="action" /> is <see langword="null" />.</exception>
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<char> chars = stackalloc char[Utf8NoBom.GetCharCount(bytes)];
Utf8NoBom.GetChars(bytes, chars);
return chars.ToString();
}
/// <summary>
/// Serializes the specified action to a Virtual Paradise action string.
/// </summary>
/// <param name="utf8ActionStream">The stream to which the action will be written.</param>
/// <param name="action">The action to serialize.</param>
/// <param name="options">Options to customize the behaviour of serialization.</param>
/// <returns>The serialized action string.</returns>
/// <exception cref="ArgumentNullException">
/// <para><paramref name="utf8ActionStream" /> is <see langword="null" />.</para>
/// -or-
/// <para><paramref name="action" /> is <see langword="null" />.</para>
/// </exception>
/// <exception cref="NotSupportedException"><paramref name="utf8ActionStream" /> does not support writing.</exception>
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);
}
/// <summary>
/// Serializes the specified action to a Virtual Paradise action string.
/// </summary>
/// <param name="writer">The writer to which the action will be written.</param>
/// <param name="action">The action to serialize.</param>
/// <param name="options">Options to customize the behaviour of serialization.</param>
/// <returns></returns>
/// <exception cref="ArgumentNullException">
/// <para><paramref name="writer" /> is <see langword="null" />.</para>
/// -or-
/// <para><paramref name="action" /> is <see langword="null" />.</para>
/// </exception>
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<Trigger> 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<TriggerAttribute>();
if (attribute is null)
{
return;
}
writer.Write(attribute.TriggerName);
IReadOnlyList<Command> 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<CommandAttribute>();
if (attribute is null)
{
return;
}
if (useDefinedConverter)
{
writer.Write(attribute.CommandName);
ICollection<Type> commandConverters = options.CommandConverters;
if (type.GetCustomAttribute<CommandConverterAttribute>() 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<Type> 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<FlagAttribute>();
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<ParameterAttribute>();
if (attribute is not null)
{
actual.Add((member, attribute));
}
}
}
private static void SerializeProperties(Utf8ActionWriter writer,
Command command,
PropertyInfo[] members,
ActionSerializerOptions options)
{
ICollection<Type> valueConverters = options.ValueConverters;
var defaultInstance = (Command)Activator.CreateInstance(command.GetType())!;
foreach (PropertyInfo member in members)
{
var attribute = member.GetCustomAttribute<PropertyAttribute>();
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<Type> valueConverters)
{
object? value = member.GetValue(command);
if (value is null)
{
return false;
}
Type type = member.PropertyType;
object?[]? args = null;
if (member.GetCustomAttribute<ValueConverterAttribute>() is { } memberAttribute)
{
valueConverters = [memberAttribute.ConverterType, ..valueConverters];
args = memberAttribute.Args;
}
else if (member.PropertyType.GetCustomAttribute<ValueConverterAttribute>() 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;
}
}

View File

@ -0,0 +1,39 @@
using System.Text;
namespace VpSharp.Building.Serialization;
/// <summary>
/// Provides methods for serializing and deserializing Virtual Paradise actions.
/// </summary>
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);
/// <summary>
/// Returns a value indicating whether the specified input constitutes a Virtual Paradise boolean.
/// </summary>
/// <param name="input">The input to check.</param>
/// <returns><see langword="true" /> if the input constitutes a valid boolean; otherwise, <see langword="false" />.</returns>
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;
}
}

View File

@ -0,0 +1,56 @@
namespace VpSharp.Building.Serialization;
/// <summary>
/// Represents a class which can convert a command.
/// </summary>
/// <typeparam name="TCommand">The type of the command to convert.</typeparam>
public abstract class CommandConverter<TCommand> : CommandConverter
where TCommand : Command
{
/// <summary>
/// Initializes a new instance of the <see cref="CommandConverter{T}" /> class.
/// </summary>
protected internal CommandConverter()
{
IsValueType = typeof(TCommand).IsValueType;
}
/// <inheritdoc />
public sealed override Type? Type { get; } = typeof(TCommand);
/// <inheritdoc />
public override bool CanConvert(Type typeToConvert)
{
return typeToConvert == typeof(TCommand);
}
/// <summary>
/// Reads the command to from specified reader.
/// </summary>
/// <param name="reader">The reader.</param>
/// <param name="options">Options which customize the serialization options.</param>
public abstract TCommand? Read(TextReader reader, ActionSerializerOptions options);
/// <inheritdoc />
public override Command? Read(TextReader reader, Type typeToConvert, ActionSerializerOptions options)
{
return CanConvert(typeToConvert) ? Read(reader, options) : null;
}
/// <summary>
/// Writes the command to the specified writer.
/// </summary>
/// <param name="writer">The writer.</param>
/// <param name="value">The command.</param>
/// <param name="options">Options which customize the serialization options.</param>
public abstract void Write(Utf8ActionWriter writer, TCommand? value, ActionSerializerOptions options);
/// <inheritdoc />
public override void Write(Utf8ActionWriter writer, Type typeToConvert, Command? value, ActionSerializerOptions options)
{
if (CanConvert(typeToConvert) && value is TCommand actual)
{
Write(writer, actual, options);
}
}
}

View File

@ -0,0 +1,132 @@
namespace VpSharp.Building.Serialization;
/// <summary>
/// Represents a class which can convert a command.
/// </summary>
public abstract class CommandConverter
{
/// <summary>
/// Initializes a new instance of the <see cref="CommandConverter" /> class.
/// </summary>
protected CommandConverter()
{
IsInternalConverter = GetType().Assembly == typeof(CommandConverter).Assembly;
}
/// <summary>
/// Gets the type being converted by the current converter instance.
/// </summary>
/// <value>The command type.</value>
public abstract Type? Type { get; }
internal bool IsInternalConverter { get; init; }
internal bool IsValueType { get; init; }
/// <summary>
/// When overridden in a derived class, determines whether the converter instance can convert the specified object type.
/// </summary>
/// <param name="typeToConvert">
/// The type of the object to check whether it can be converted by this converter instance.
/// </param>
/// <returns>
/// <see langword="true" /> if the instance can convert the specified object type; otherwise, <see langword="false" />.
/// </returns>
public abstract bool CanConvert(Type typeToConvert);
/// <summary>
/// Reads the command to from specified reader.
/// </summary>
/// <param name="reader">The reader.</param>
/// <param name="typeToConvert">The type of the command being converted.</param>
/// <param name="options">Options which customize the serialization options.</param>
/// <returns>The deserialized command, or <see langword="null" /> if deserialization failed.</returns>
public abstract Command? Read(TextReader reader, Type typeToConvert, ActionSerializerOptions options);
/// <summary>
/// Writes the command to the specified writer.
/// </summary>
/// <param name="writer">The writer.</param>
/// <param name="typeToConvert">The type of the command being converted.</param>
/// <param name="value">The value.</param>
/// <param name="options">Options which customize the serialization options.</param>
public abstract void Write(Utf8ActionWriter writer, Type typeToConvert, Command? value, ActionSerializerOptions options);
/// <summary>
/// Reads the command to from specified reader using the default deserializer.
/// </summary>
/// <param name="reader">The reader.</param>
/// <param name="command">The command being converted.</param>
/// <param name="options">Options which customize the serialization options.</param>
protected void DefaultRead<T>(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;
}
}
/// <summary>
/// Reads the command to from specified reader using the default deserializer.
/// </summary>
/// <param name="reader">The reader.</param>
/// <param name="typeToConvert">The type of the command being converted.</param>
/// <param name="options">Options which customize the serialization options.</param>
/// <returns>The deserialized command, or <see langword="null" /> if deserialization failed.</returns>
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;
}
/// <summary>
/// Writes the command to the specified writer using the default command converter.
/// </summary>
/// <param name="writer">The writer.</param>
/// <param name="value">The value.</param>
/// <param name="options">Options which customize the serialization options.</param>
protected void DefaultWrite(Utf8ActionWriter writer, Command? value, ActionSerializerOptions options)
{
if (value is not null)
{
ActionSerializer.SerializeCommand(writer, value, options, false);
}
}
}

View File

@ -0,0 +1,16 @@
namespace VpSharp.Building.Serialization;
/// <summary>
/// Defines the converter to use for this command.
/// </summary>
[AttributeUsage(AttributeTargets.Class)]
public sealed class CommandConverterAttribute<T> : CommandConverterAttribute
where T : CommandConverter
{
/// <summary>
/// Initializes a new instance of the <see cref="CommandConverterAttribute" /> class.
/// </summary>
public CommandConverterAttribute() : base(typeof(T))
{
}
}

View File

@ -0,0 +1,37 @@
namespace VpSharp.Building.Serialization;
/// <summary>
/// Defines the converter to use for this command.
/// </summary>
[AttributeUsage(AttributeTargets.Class)]
public class CommandConverterAttribute : Attribute
{
/// <summary>
/// Initializes a new instance of the <see cref="CommandConverterAttribute" /> class.
/// </summary>
/// <param name="converterType">The type of the converter to use.</param>
/// <exception cref="ArgumentNullException"><paramref name="converterType" /> is <see langword="null" />.</exception>
/// <exception cref="ArgumentException">
/// <paramref name="converterType" /> does not inherit <see cref="CommandConverter" />.
/// </exception>
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;
}
/// <summary>
/// Gets the type of the converter to use.
/// </summary>
/// <value>The type of the converter.</value>
public Type ConverterType { get; }
}

View File

@ -0,0 +1,57 @@
namespace VpSharp.Building.Serialization;
/// <summary>
/// An enumeration of token types.
/// </summary>
public enum TokenType
{
/// <summary>
/// No token type.
/// </summary>
None,
/// <summary>
/// The token is a standard identifier or argument.
/// </summary>
Text,
/// <summary>
/// The token is a trigger name.
/// </summary>
TriggerName,
/// <summary>
/// The token is a command name.
/// </summary>
CommandName,
/// <summary>
/// The token is a number.
/// </summary>
Number,
/// <summary>
/// The token is a boolean.
/// </summary>
Boolean,
/// <summary>
/// The token is an escaped string.
/// </summary>
String,
/// <summary>
/// The token marks the end of a command.
/// </summary>
EndCommand,
/// <summary>
/// The token marks the end of a trigger.
/// </summary>
EndTrigger,
/// <summary>
/// The token is the end of the file.
/// </summary>
EndOfFile
}

View File

@ -0,0 +1,116 @@
using System.Text;
namespace VpSharp.Building.Serialization;
/// <summary>
/// Represents a text writer which writes Virtual Paradise action string components.
/// </summary>
public sealed class Utf8ActionWriter : TextWriter
{
private readonly Stream _stream;
private readonly ActionSerializerOptions _options;
/// <summary>
/// Initializes a new instance of the <see cref="Utf8ActionWriter" /> class.
/// </summary>
/// <param name="stream">The stream.</param>
/// <param name="options">Options to customize the serialization behaviour.</param>
public Utf8ActionWriter(Stream stream, ActionSerializerOptions? options)
{
_stream = stream;
_options = options ?? new ActionSerializerOptions();
}
/// <inheritdoc />
public override Encoding Encoding { get; } = Encoding.UTF8;
/// <inheritdoc />
protected override void Dispose(bool disposing)
{
_stream.Dispose();
base.Dispose(disposing);
}
/// <inheritdoc />
public override void Write(char value)
{
Span<char> span = [value];
int byteCount = Encoding.GetByteCount(span);
Span<byte> bytes = stackalloc byte[byteCount];
Encoding.GetBytes(span, bytes);
_stream.Write(bytes);
}
/// <summary>
/// Copies the contents of the specified stream to the current writer. The stream will be read from the current position,
/// and consumed in full.
/// </summary>
/// <param name="stream">The stream whose contents to copy.</param>
/// <exception cref="ArgumentNullException"><paramref name="stream" /> is <see langword="null" />.</exception>
/// <exception cref="NotSupportedException"><paramref name="stream" /> does not support reading.</exception>
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);
}
/// <summary>
/// Writes a property in the format <c>name=value</c> to the current writer.
/// </summary>
/// <param name="propertyName">The property name.</param>
/// <param name="propertyValue">The property value.</param>
/// <typeparam name="TValue">The type of <paramref name="propertyValue" />.</typeparam>
/// <exception cref="ArgumentNullException"><paramref name="propertyName" /> is <see langword="null" />.</exception>
/// <exception cref="ArgumentException"><paramref name="propertyName" /> is empty or consists of only whitespace.</exception>
public void WriteProperty<TValue>(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);
}
/// <summary>
/// Writes a convertible value to the current writer.
/// </summary>
/// <param name="value">The value to write.</param>
/// <typeparam name="TValue">The type of <paramref name="value" />.</typeparam>
public void WriteValue<TValue>(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;
}
}
}

View File

@ -0,0 +1,55 @@
namespace VpSharp.Building.Serialization;
/// <summary>
/// Represents a class which can convert a value.
/// </summary>
/// <typeparam name="TValue">The type of the value to convert.</typeparam>
public abstract class ValueConverter<TValue> : ValueConverter
{
/// <summary>
/// Initializes a new instance of the <see cref="ValueConverter{T}" /> class.
/// </summary>
protected internal ValueConverter()
{
IsValueType = typeof(TValue).IsValueType;
}
/// <inheritdoc />
public sealed override Type? Type { get; } = typeof(TValue);
/// <inheritdoc />
public override bool CanConvert(Type typeToConvert)
{
return typeToConvert == typeof(TValue);
}
/// <summary>
/// Reads the value to from specified reader.
/// </summary>
/// <param name="reader">The reader.</param>
/// <param name="options">Options which customize the serialization options.</param>
public abstract TValue? Read(TextReader reader, ActionSerializerOptions options);
/// <inheritdoc />
public override object? Read(TextReader reader, Type typeToConvert, ActionSerializerOptions options)
{
return CanConvert(typeToConvert) ? Read(reader, options) : null;
}
/// <summary>
/// Writes the value to the specified writer.
/// </summary>
/// <param name="writer">The writer.</param>
/// <param name="value">The value.</param>
/// <param name="options">Options which customize the serialization options.</param>
public abstract void Write(Utf8ActionWriter writer, TValue? value, ActionSerializerOptions options);
/// <inheritdoc />
public override void Write(Utf8ActionWriter writer, Type typeToConvert, object? value, ActionSerializerOptions options)
{
if (CanConvert(typeToConvert) && value is TValue actual)
{
Write(writer, actual, options);
}
}
}

View File

@ -0,0 +1,53 @@
namespace VpSharp.Building.Serialization;
/// <summary>
/// Represents a class which can convert a value.
/// </summary>
public abstract class ValueConverter
{
/// <summary>
/// Initializes a new instance of the <see cref="ValueConverter" /> class.
/// </summary>
protected ValueConverter()
{
IsInternalConverter = GetType().Assembly == typeof(ValueConverter).Assembly;
}
/// <summary>
/// Gets the type being converted by the current converter instance.
/// </summary>
/// <value>The command type.</value>
public abstract Type? Type { get; }
internal bool IsInternalConverter { get; init; }
internal bool IsValueType { get; init; }
/// <summary>
/// When overridden in a derived class, determines whether the converter instance can convert the specified object type.
/// </summary>
/// <param name="typeToConvert">
/// The type of the object to check whether it can be converted by this converter instance.
/// </param>
/// <returns>
/// <see langword="true" /> if the instance can convert the specified object type; otherwise, <see langword="false" />.
/// </returns>
public abstract bool CanConvert(Type typeToConvert);
/// <summary>
/// Reads the value to from specified reader.
/// </summary>
/// <param name="reader">The reader.</param>
/// <param name="typeToConvert">The type of the value being converted.</param>
/// <param name="options">Options which customize the serialization options.</param>
public abstract object? Read(TextReader reader, Type typeToConvert, ActionSerializerOptions options);
/// <summary>
/// Writes the value to the specified writer.
/// </summary>
/// <param name="writer">The writer.</param>
/// <param name="typeToConvert">The type of the value being converted.</param>
/// <param name="value">The value.</param>
/// <param name="options">Options which customize the serialization options.</param>
public abstract void Write(Utf8ActionWriter writer, Type typeToConvert, object? value, ActionSerializerOptions options);
}

View File

@ -0,0 +1,17 @@
namespace VpSharp.Building.Serialization;
/// <summary>
/// Defines the value converter to use for this property.
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Property)]
public sealed class ValueConverterAttribute<TValueConverter> : ValueConverterAttribute
where TValueConverter : ValueConverter
{
/// <summary>
/// Initializes a new instance of the <see cref="ValueConverterAttribute{TValueConverter}" /> class.
/// </summary>
/// <param name="args">Arguments to pass to the constructor.</param>
public ValueConverterAttribute(params object?[]? args) : base(typeof(TValueConverter), args)
{
}
}

View File

@ -0,0 +1,45 @@
namespace VpSharp.Building.Serialization;
/// <summary>
/// Defines the value converter to use for this property.
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Property)]
public class ValueConverterAttribute : Attribute
{
/// <summary>
/// Initializes a new instance of the <see cref="ValueConverterAttribute" /> class.
/// </summary>
/// <param name="converterType">The type of the converter to use.</param>
/// <param name="args">Arguments to pass to the constructor.</param>
/// <exception cref="ArgumentNullException"><paramref name="converterType" /> is <see langword="null" />.</exception>
/// <exception cref="ArgumentException">
/// <paramref name="converterType" /> does not inherit <see cref="ValueConverter" />.
/// </exception>
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;
}
/// <summary>
/// Gets the arguments to be passed to the converter's constructor.
/// </summary>
/// <value>An array of objects to be passed to the constructor.</value>
public object?[]? Args { get; }
/// <summary>
/// Gets the type of the converter to use.
/// </summary>
/// <value>The type of the converter.</value>
public Type ConverterType { get; }
}

View File

@ -0,0 +1,25 @@
using System.ComponentModel;
namespace VpSharp.Building;
/// <summary>
/// An enumeration of text alignments.
/// </summary>
public enum TextAlignment
{
/// <summary>
/// Center text alignment.
/// </summary>
[Description("Center text alignment.")]
Center,
/// <summary>
/// Left text alignment.
/// </summary>
[Description("Left text alignment.")] Left,
/// <summary>
/// Right text alignment.
/// </summary>
[Description("Right text alignment.")] Right
}

View File

@ -0,0 +1,55 @@
namespace VpSharp.Building;
/// <summary>
/// Represents a trigger.
/// </summary>
public abstract class Trigger
{
/// <summary>
/// Gets the commands in this trigger.
/// </summary>
/// <value>A read-only view of the commands in this trigger.</value>
public IReadOnlyList<Command> Commands { get; internal set; } = [];
}
/// <summary>
/// Represents an unknown trigger.
/// </summary>
internal sealed class UnknownTrigger : Trigger
{
/// <summary>
/// Initializes a new instance of the <see cref="UnknownTrigger" /> class.
/// </summary>
/// <param name="triggerName">The name of the trigger.</param>
public UnknownTrigger(string triggerName)
{
TriggerName = triggerName;
}
/// <summary>
/// Gets the name of the trigger.
/// </summary>
/// <value>The name of the trigger.</value>
public string TriggerName { get; }
}
/// <summary>
/// Represents an unknown command.
/// </summary>
internal sealed class UnknownCommand : Command
{
/// <summary>
/// Initializes a new instance of the <see cref="UnknownCommand" /> class.
/// </summary>
/// <param name="commandName">The name of the command.</param>
public UnknownCommand(string commandName)
{
CommandName = commandName;
}
/// <summary>
/// Gets the name of the command.
/// </summary>
/// <value>The name of the command.</value>
public string CommandName { get; }
}

View File

@ -0,0 +1,35 @@
namespace VpSharp.Building;
/// <summary>
/// Defines the name of a trigger.
/// </summary>
[AttributeUsage(AttributeTargets.Class)]
public sealed class TriggerAttribute : Attribute
{
/// <summary>
/// Initializes a new instance of the <see cref="TriggerAttribute" /> class.
/// </summary>
/// <param name="triggerName">The name of the trigger.</param>
/// <exception cref="ArgumentNullException"><paramref name="triggerName" /> is <see langword="null" />.</exception>
/// <exception cref="ArgumentException"><paramref name="triggerName" /> is empty, or consists only of whitespace.</exception>
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;
}
/// <summary>
/// Gets the name of the trigger.
/// </summary>
/// <value>The name of the trigger.</value>
public string TriggerName { get; }
}

View File

@ -0,0 +1,38 @@
using VpSharp.Building.Extensions;
using VpSharp.Building.Serialization;
namespace VpSharp.Building.ValueConverters;
/// <summary>
/// Represents a converter which can convert values of type <see cref="bool" />.
/// </summary>
public sealed class BooleanValueConverter : ValueConverter<bool>
{
/// <inheritdoc />
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));
}
/// <inheritdoc />
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;
}
}
}

View File

@ -0,0 +1,23 @@
using VpSharp.Building.Extensions;
using VpSharp.Building.Serialization;
namespace VpSharp.Building.ValueConverters;
/// <summary>
/// Represents a converter which can convert values of type <see cref="byte" />.
/// </summary>
public sealed class ByteValueConverter : ValueConverter<byte>
{
/// <inheritdoc />
public override byte Read(TextReader reader, ActionSerializerOptions options)
{
string token = reader.ReadToSpace();
return byte.Parse(token);
}
/// <inheritdoc />
public override void Write(Utf8ActionWriter writer, byte value, ActionSerializerOptions options)
{
writer.Write(value);
}
}

View File

@ -0,0 +1,21 @@
using VpSharp.Building.Serialization;
namespace VpSharp.Building.ValueConverters;
/// <summary>
/// Represents a converter which can convert values of type <see cref="char" />.
/// </summary>
public sealed class CharValueConverter : ValueConverter<char>
{
/// <inheritdoc />
public override char Read(TextReader reader, ActionSerializerOptions options)
{
return (char)reader.Read();
}
/// <inheritdoc />
public override void Write(Utf8ActionWriter writer, char value, ActionSerializerOptions options)
{
writer.Write(value);
}
}

View File

@ -0,0 +1,38 @@
using System.Drawing;
using VpSharp.Building.Extensions;
using VpSharp.Building.Serialization;
namespace VpSharp.Building.ValueConverters;
/// <summary>
/// Represents a converter which can convert values of type <see cref="Color" />.
/// </summary>
public sealed class ColorValueConverter : ValueConverter<Color>
{
/// <inheritdoc />
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}");
}
/// <inheritdoc />
public override void Write(Utf8ActionWriter writer, Color value, ActionSerializerOptions options)
{
int argb = value.ToArgb();
if (VirtualParadiseColors.KnownColors.ContainsValue(argb))
{
KeyValuePair<string, int> pair = VirtualParadiseColors.KnownColors.First(c => c.Value == argb);
writer.Write(pair.Key);
return;
}
Span<char> 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);
}
}

View File

@ -0,0 +1,23 @@
using VpSharp.Building.Extensions;
using VpSharp.Building.Serialization;
namespace VpSharp.Building.ValueConverters;
/// <summary>
/// Represents a converter which can convert values of type <see cref="decimal" />.
/// </summary>
public sealed class DecimalValueConverter : ValueConverter<decimal>
{
/// <inheritdoc />
public override decimal Read(TextReader reader, ActionSerializerOptions options)
{
string token = reader.ReadToSpace();
return decimal.Parse(token);
}
/// <inheritdoc />
public override void Write(Utf8ActionWriter writer, decimal value, ActionSerializerOptions options)
{
writer.Write(value);
}
}

View File

@ -0,0 +1,23 @@
using VpSharp.Building.Extensions;
using VpSharp.Building.Serialization;
namespace VpSharp.Building.ValueConverters;
/// <summary>
/// Represents a converter which can convert values of type <see cref="double" />.
/// </summary>
public sealed class DoubleValueConverter : ValueConverter<double>
{
/// <inheritdoc />
public override double Read(TextReader reader, ActionSerializerOptions options)
{
string token = reader.ReadToSpace();
return double.Parse(token);
}
/// <inheritdoc />
public override void Write(Utf8ActionWriter writer, double value, ActionSerializerOptions options)
{
writer.Write(value);
}
}

View File

@ -0,0 +1,23 @@
using VpSharp.Building.Extensions;
using VpSharp.Building.Serialization;
namespace VpSharp.Building.ValueConverters;
/// <summary>
/// Represents a converter which can convert values of type <see cref="short" />.
/// </summary>
public sealed class Int16ValueConverter : ValueConverter<short>
{
/// <inheritdoc />
public override short Read(TextReader reader, ActionSerializerOptions options)
{
string token = reader.ReadToSpace();
return short.Parse(token);
}
/// <inheritdoc />
public override void Write(Utf8ActionWriter writer, short value, ActionSerializerOptions options)
{
writer.Write(value);
}
}

View File

@ -0,0 +1,23 @@
using VpSharp.Building.Extensions;
using VpSharp.Building.Serialization;
namespace VpSharp.Building.ValueConverters;
/// <summary>
/// Represents a converter which can convert values of type <see cref="int" />.
/// </summary>
public sealed class Int32ValueConverter : ValueConverter<int>
{
/// <inheritdoc />
public override int Read(TextReader reader, ActionSerializerOptions options)
{
string token = reader.ReadToSpace();
return int.Parse(token);
}
/// <inheritdoc />
public override void Write(Utf8ActionWriter writer, int value, ActionSerializerOptions options)
{
writer.Write(value);
}
}

View File

@ -0,0 +1,23 @@
using VpSharp.Building.Extensions;
using VpSharp.Building.Serialization;
namespace VpSharp.Building.ValueConverters;
/// <summary>
/// Represents a converter which can convert values of type <see cref="long" />.
/// </summary>
public sealed class Int64ValueConverter : ValueConverter<long>
{
/// <inheritdoc />
public override long Read(TextReader reader, ActionSerializerOptions options)
{
string token = reader.ReadToSpace();
return long.Parse(token);
}
/// <inheritdoc />
public override void Write(Utf8ActionWriter writer, long value, ActionSerializerOptions options)
{
writer.Write(value);
}
}

View File

@ -0,0 +1,24 @@
using VpSharp.Building.Extensions;
using VpSharp.Building.Serialization;
namespace VpSharp.Building.ValueConverters;
/// <summary>
/// Represents a converter which can convert values of type <see cref="sbyte" />.
/// </summary>
[CLSCompliant(false)]
public sealed class SByteValueConverter : ValueConverter<sbyte>
{
/// <inheritdoc />
public override sbyte Read(TextReader reader, ActionSerializerOptions options)
{
string token = reader.ReadToSpace();
return sbyte.Parse(token);
}
/// <inheritdoc />
public override void Write(Utf8ActionWriter writer, sbyte value, ActionSerializerOptions options)
{
writer.Write(value);
}
}

View File

@ -0,0 +1,23 @@
using VpSharp.Building.Extensions;
using VpSharp.Building.Serialization;
namespace VpSharp.Building.ValueConverters;
/// <summary>
/// Represents a converter which can convert values of type <see cref="float" />.
/// </summary>
public sealed class SingleValueConverter : ValueConverter<float>
{
/// <inheritdoc />
public override float Read(TextReader reader, ActionSerializerOptions options)
{
string token = reader.ReadToSpace();
return float.Parse(token);
}
/// <inheritdoc />
public override void Write(Utf8ActionWriter writer, float value, ActionSerializerOptions options)
{
writer.Write(value);
}
}

View File

@ -0,0 +1,77 @@
using Cysharp.Text;
using VpSharp.Building.Extensions;
using VpSharp.Building.Serialization;
namespace VpSharp.Building.ValueConverters;
/// <summary>
/// Represents a converter which can convert values of type <see cref="string" />.
/// </summary>
public sealed class StringValueConverter : ValueConverter<string>
{
private readonly bool _escape;
/// <summary>
/// Initializes a new instance of the <see cref="StringValueConverter" /> class.
/// </summary>
public StringValueConverter()
{
}
/// <summary>
/// Initializes a new instance of the <see cref="StringValueConverter" /> class.
/// </summary>
/// <param name="escape"><see langword="true" /> if the string should be escaped; otherwise, <see langword="false" />.</param>
public StringValueConverter(bool escape)
{
_escape = escape;
}
/// <inheritdoc />
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();
}
/// <inheritdoc />
public override void Write(Utf8ActionWriter writer, string? value, ActionSerializerOptions options)
{
if (_escape)
{
writer.Write('"');
}
writer.Write(value);
if (_escape)
{
writer.Write('"');
}
}
}

View File

@ -0,0 +1,24 @@
using VpSharp.Building.Extensions;
using VpSharp.Building.Serialization;
namespace VpSharp.Building.ValueConverters;
/// <summary>
/// Represents a converter which can convert values of type <see cref="ushort" />.
/// </summary>
[CLSCompliant(false)]
public sealed class UInt16ValueConverter : ValueConverter<ushort>
{
/// <inheritdoc />
public override ushort Read(TextReader reader, ActionSerializerOptions options)
{
string token = reader.ReadToSpace();
return ushort.Parse(token);
}
/// <inheritdoc />
public override void Write(Utf8ActionWriter writer, ushort value, ActionSerializerOptions options)
{
writer.Write(value);
}
}

View File

@ -0,0 +1,24 @@
using VpSharp.Building.Extensions;
using VpSharp.Building.Serialization;
namespace VpSharp.Building.ValueConverters;
/// <summary>
/// Represents a converter which can convert values of type <see cref="uint" />.
/// </summary>
[CLSCompliant(false)]
public sealed class UInt32ValueConverter : ValueConverter<uint>
{
/// <inheritdoc />
public override uint Read(TextReader reader, ActionSerializerOptions options)
{
string token = reader.ReadToSpace();
return uint.Parse(token);
}
/// <inheritdoc />
public override void Write(Utf8ActionWriter writer, uint value, ActionSerializerOptions options)
{
writer.Write(value);
}
}

View File

@ -0,0 +1,24 @@
using VpSharp.Building.Extensions;
using VpSharp.Building.Serialization;
namespace VpSharp.Building.ValueConverters;
/// <summary>
/// Represents a converter which can convert values of type <see cref="ulong" />.
/// </summary>
[CLSCompliant(false)]
public sealed class UInt64ValueConverter : ValueConverter<ulong>
{
/// <inheritdoc />
public override ulong Read(TextReader reader, ActionSerializerOptions options)
{
string token = reader.ReadToSpace();
return ulong.Parse(token);
}
/// <inheritdoc />
public override void Write(Utf8ActionWriter writer, ulong value, ActionSerializerOptions options)
{
writer.Write(value);
}
}

View File

@ -0,0 +1,45 @@
using System.Drawing;
using System.Numerics;
using VpSharp.Building.Extensions;
using VpSharp.Building.Serialization;
namespace VpSharp.Building.ValueConverters;
/// <summary>
/// Represents a converter which can convert values of type <see cref="Color" />.
/// </summary>
public sealed class Vector3ValueConverter : ValueConverter<Vector3>
{
/// <inheritdoc />
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);
}
/// <inheritdoc />
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);
}
}
}

View File

@ -0,0 +1,171 @@
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
namespace VpSharp.Building;
/// <summary>
/// Represents a Virtual Paradise object action string.
/// </summary>
public sealed class VirtualParadiseAction
{
/// <summary>
/// An empty action.
/// </summary>
public static readonly VirtualParadiseAction Empty = new();
private readonly Dictionary<string, List<Trigger>> _triggers = [];
/// <summary>
/// Gets the triggers in this action.
/// </summary>
/// <value>A read-only view of the triggers in this action.</value>
public IReadOnlyList<Trigger> Triggers
{
get => _triggers.Values.SelectMany(v => v).ToArray();
internal set
{
_triggers.Clear();
foreach (Trigger trigger in value)
{
var attribute = trigger.GetType().GetCustomAttribute<TriggerAttribute>();
if (attribute is null)
{
continue;
}
if (!_triggers.TryGetValue(attribute.TriggerName, out List<Trigger>? triggers))
{
triggers = new List<Trigger>();
_triggers[attribute.TriggerName] = triggers;
}
triggers.Add(trigger);
}
}
}
/// <summary>
/// Gets the first trigger of the specified type.
/// </summary>
/// <param name="triggerType">The type of the trigger to return.</param>
/// <returns>The matching trigger, or <see langword="null" /> if no matching trigger was found.</returns>
/// <exception cref="ArgumentNullException"><paramref name="triggerType" /> is <see langword="null" />.</exception>
/// <exception cref="ArgumentException"><paramref name="triggerType" /> does not inherit <see cref="Trigger" />.</exception>
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;
}
/// <summary>
/// Gets all triggers of the specified type.
/// </summary>
/// <param name="triggerType">The type of the trigger to return.</param>
/// <returns>An ordered collection of all the matching triggers.</returns>
/// <exception cref="ArgumentNullException"><paramref name="triggerType" /> is <see langword="null" />.</exception>
/// <exception cref="ArgumentException"><paramref name="triggerType" /> does not inherit <see cref="Trigger" />.</exception>
public IReadOnlyList<Trigger> 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<Trigger>();
foreach (Trigger trigger in Triggers)
{
if (trigger.GetType() == triggerType)
{
triggers.Add(trigger);
}
}
return triggers.AsReadOnly();
}
/// <summary>
/// Gets the first trigger of the specified type.
/// </summary>
/// <typeparam name="T">The type of the trigger to return.</typeparam>
/// <returns>The matching trigger, or <see langword="null" /> if no matching trigger was found.</returns>
public T? GetTrigger<T>() where T : Trigger
{
return (T?)GetTrigger(typeof(T));
}
/// <summary>
/// Gets all triggers of the specified type.
/// </summary>
/// <typeparam name="T">The type of the trigger to return.</typeparam>
/// <returns>An ordered collection of all the matching triggers.</returns>
public IReadOnlyList<T> GetTriggers<T>() where T : Trigger
{
return (IReadOnlyList<T>)GetTriggers(typeof(T));
}
/// <summary>
/// Attempts to find the first trigger of the specified type.
/// </summary>
/// <param name="triggerType">The type of the trigger to return.</param>
/// <param name="trigger">
/// When this method returns, contains the first matching trigger whose type is equal to <paramref name="triggerType" />,
/// if such a trigger exists; otherwise, <see langword="null" />.
/// </param>
/// <returns><see langword="true" /> if a matching trigger was found; otherwise, <see langword="false." /></returns>
/// <exception cref="ArgumentNullException"></exception>
/// <exception cref="ArgumentException"></exception>
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;
}
/// <summary>
/// Attempts to find the first trigger of the specified type.
/// </summary>
/// <param name="trigger">
/// When this method returns, contains the first matching trigger whose type is equal to <paramref name="triggerType" />,
/// if such a trigger exists; otherwise, <see langword="null" />.
/// </param>
/// <typeparam name="T">The type of the trigger to return.</typeparam>
/// <returns><see langword="true" /> if a matching trigger was found; otherwise, <see langword="false." /></returns>
public bool TryGetTrigger<T>([NotNullWhen(true)] out T? trigger) where T : Trigger
{
trigger = GetTrigger<T>();
return trigger is not null;
}
}

View File

@ -0,0 +1,119 @@
namespace VpSharp.Building;
/// <summary>
/// Represents a mutable <see cref="VirtualParadiseAction" />.
/// </summary>
public sealed class VirtualParadiseActionBuilder
{
private Trigger? _currentTrigger;
/// <summary>
/// Initializes a new instance of the <see cref="VirtualParadiseActionBuilder" /> class.
/// </summary>
public VirtualParadiseActionBuilder()
{
}
/// <summary>
/// Initializes a new instance of the <see cref="VirtualParadiseActionBuilder" /> class.
/// </summary>
/// <param name="action">The existing action.</param>
/// <exception cref="ArgumentNullException"><paramref name="action" /> is <see langword="null" />.</exception>
public VirtualParadiseActionBuilder(VirtualParadiseAction action)
{
if (action is null)
{
throw new ArgumentNullException(nameof(action));
}
Triggers = [..action.Triggers];
Commands = Triggers.SelectMany(t => t.Commands).ToList();
}
/// <summary>
/// Gets or sets the list of commands in the current action.
/// </summary>
/// <value>The list of commands.</value>
public IList<Command> Commands { get; set; } = [];
/// <summary>
/// Gets or sets the list of triggers in this action.
/// </summary>
/// <value>The list of triggers.</value>
public IList<Trigger> Triggers { get; set; } = [];
/// <summary>
/// Adds a command to this action.
/// </summary>
/// <param name="command">The command to add.</param>
/// <returns>The current <see cref="VirtualParadiseActionBuilder" /> instance.</returns>
/// <exception cref="ArgumentNullException"><paramref name="command" /> is <see langword="null" />.</exception>
public VirtualParadiseActionBuilder AddCommand(Command command)
{
if (command is null)
{
throw new ArgumentNullException(nameof(command));
}
// ReSharper disable once NullCoalescingConditionIsAlwaysNotNullAccordingToAPIContract
Commands ??= [];
Commands.Add(command);
return this;
}
/// <summary>
/// Adds a trigger to this action.
/// </summary>
/// <typeparam name="TTrigger">The type of the trigger to add.</typeparam>
/// <returns>The current <see cref="VirtualParadiseActionBuilder" /> instance.</returns>
public VirtualParadiseActionBuilder AddTrigger<TTrigger>()
where TTrigger : Trigger
{
if (_currentTrigger is not null)
{
_currentTrigger.Commands = Commands.ToArray();
}
var trigger = Activator.CreateInstance<TTrigger>();
return AddTrigger(trigger);
}
/// <summary>
/// Adds a trigger to this action.
/// </summary>
/// <param name="trigger">The trigger to add.</param>
/// <returns>The current <see cref="VirtualParadiseActionBuilder" /> instance.</returns>
/// <exception cref="ArgumentNullException"><paramref name="trigger" /> is <see langword="null" />.</exception>
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;
}
/// <summary>
/// Builds the action.
/// </summary>
/// <returns>The newly-built action.</returns>
public VirtualParadiseAction Build()
{
if (_currentTrigger is not null)
{
_currentTrigger.Commands = Commands.ToArray();
}
return new VirtualParadiseAction { Triggers = [..Triggers] };
}
}

View File

@ -0,0 +1,308 @@
using System.Drawing;
namespace VpSharp.Building;
/// <summary>
/// Offers predefined named colors supported by Virtual Paradise.
/// </summary>
public static class VirtualParadiseColors
{
internal static readonly Dictionary<string, int> 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()
};
/// <summary>
/// The color #70DB93.
/// </summary>
public static Color Aquamarine
{
get => Color.FromArgb(0x70, 0xDB, 0x93);
}
/// <summary>
/// The color #000000.
/// </summary>
public static Color Black
{
get => Color.FromArgb(0x00, 0x00, 0x00);
}
/// <summary>
/// The color #0000FF.
/// </summary>
public static Color Blue
{
get => Color.FromArgb(0x00, 0x00, 0xFF);
}
/// <summary>
/// The color #B5A642.
/// </summary>
public static Color Brass
{
get => Color.FromArgb(0xB5, 0xA6, 0x42);
}
/// <summary>
/// The color #8C7853.
/// </summary>
public static Color Bronze
{
get => Color.FromArgb(0x8C, 0x78, 0x53);
}
/// <summary>
/// The color #B87333.
/// </summary>
public static Color Copper
{
get => Color.FromArgb(0xB8, 0x73, 0x33);
}
/// <summary>
/// The color #00FFFF.
/// </summary>
public static Color Cyan
{
get => Color.FromArgb(0x00, 0xFF, 0xFF);
}
/// <summary>
/// The color #303030.
/// </summary>
public static Color DarkGrey
{
get => Color.FromArgb(0x30, 0x30, 0x30);
}
/// <summary>
/// The color #0000C0.
/// </summary>
public static Color DefaultSignBackColor
{
get => Color.FromArgb(0x00, 0x00, 0xC0);
}
/// <summary>
/// The color #238E23.
/// </summary>
public static Color ForestGreen
{
get => Color.FromArgb(0x23, 0x8E, 0x23);
}
/// <summary>
/// The color #CD7F32.
/// </summary>
public static Color Gold
{
get => Color.FromArgb(0xCD, 0x7F, 0x32);
}
/// <summary>
/// The color #00FF00.
/// </summary>
public static Color Green
{
get => Color.FromArgb(0x00, 0xFF, 0x00);
}
/// <summary>
/// The color #707070.
/// </summary>
public static Color Grey
{
get => Color.FromArgb(0x70, 0x70, 0x70);
}
/// <summary>
/// The color #C0C0C0.
/// </summary>
public static Color LightGrey
{
get => Color.FromArgb(0xC0, 0xC0, 0xC0);
}
/// <summary>
/// The color #FF00FF.
/// </summary>
public static Color Magenta
{
get => Color.FromArgb(0xFF, 0x00, 0xFF);
}
/// <summary>
/// The color #8E236B.
/// </summary>
public static Color Maroon
{
get => Color.FromArgb(0x8E, 0x23, 0x6B);
}
/// <summary>
/// The color #23238E.
/// </summary>
public static Color NavyBlue
{
get => Color.FromArgb(0x23, 0x23, 0x8E);
}
/// <summary>
/// The color #FF7F00.
/// </summary>
public static Color Orange
{
get => Color.FromArgb(0xFF, 0x7F, 0x00);
}
/// <summary>
/// The color #FF2400.
/// </summary>
public static Color OrangeRed
{
get => Color.FromArgb(0xFF, 0x24, 0x00);
}
/// <summary>
/// The color #DB70DB.
/// </summary>
public static Color Orchid
{
get => Color.FromArgb(0xDB, 0x70, 0xDB);
}
/// <summary>
/// The color #FF6EC7.
/// </summary>
public static Color Pink
{
get => Color.FromArgb(0xFF, 0x6E, 0xC7);
}
/// <summary>
/// The color #FF0000.
/// </summary>
public static Color Red
{
get => Color.FromArgb(0xFF, 0x00, 0x00);
}
/// <summary>
/// The color #6F4242.
/// </summary>
public static Color Salmon
{
get => Color.FromArgb(0x6F, 0x42, 0x42);
}
/// <summary>
/// The color #8C1717.
/// </summary>
public static Color Scarlet
{
get => Color.FromArgb(0x8C, 0x17, 0x17);
}
/// <summary>
/// The color #E6E8FA.
/// </summary>
public static Color Silver
{
get => Color.FromArgb(0xE6, 0xE8, 0xFA);
}
/// <summary>
/// The color #3299CC.
/// </summary>
public static Color SkyBlue
{
get => Color.FromArgb(0x32, 0x99, 0xCC);
}
/// <summary>
/// The color #DB9370.
/// </summary>
public static Color Tan
{
get => Color.FromArgb(0xDB, 0x93, 0x70);
}
/// <summary>
/// The color #007070.
/// </summary>
public static Color Teal
{
get => Color.FromArgb(0x00, 0x70, 0x70);
}
/// <summary>
/// Transparent.
/// </summary>
public static Color Transparent
{
get => Color.FromArgb(0x00, 0x00, 0x00, 0x00);
}
/// <summary>
/// The color #ADEAEA.
/// </summary>
public static Color Turquoise
{
get => Color.FromArgb(0xAD, 0xEA, 0xEA);
}
/// <summary>
/// The color #4F2F4F.
/// </summary>
public static Color Violet
{
get => Color.FromArgb(0x4F, 0x2F, 0x4F);
}
/// <summary>
/// The color #FFFFFF.
/// </summary>
public static Color White
{
get => Color.FromArgb(0xFF, 0xFF, 0xFF);
}
/// <summary>
/// The color #FFFF00.
/// </summary>
public static Color Yellow
{
get => Color.FromArgb(0xFF, 0xFF, 0x00);
}
}

View File

@ -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