mirror of
https://github.com/oliverbooth/VpSharp
synced 2024-11-09 22:55:42 +00:00
feat: add action (de)serialization
This commit is contained in:
parent
183261fedd
commit
59a9043d97
30
VpSharp.Building.Tests/VpSharp.Building.Tests.csproj
Normal file
30
VpSharp.Building.Tests/VpSharp.Building.Tests.csproj
Normal 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>
|
44
VpSharp.Building.Tests/src/ColorTests.cs
Normal file
44
VpSharp.Building.Tests/src/ColorTests.cs
Normal 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)));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
19
VpSharp.Building.Tests/src/SetupTrace.cs
Normal file
19
VpSharp.Building.Tests/src/SetupTrace.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
15
VpSharp.Building/VpSharp.Building.csproj
Normal file
15
VpSharp.Building/VpSharp.Building.csproj
Normal 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>
|
96
VpSharp.Building/src/ActionSerializerOptions.cs
Normal file
96
VpSharp.Building/src/ActionSerializerOptions.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
3
VpSharp.Building/src/Assembly.cs
Normal file
3
VpSharp.Building/src/Assembly.cs
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
|
[assembly: InternalsVisibleTo("VpSharp.Building.Tests")]
|
22
VpSharp.Building/src/BooleanMode.cs
Normal file
22
VpSharp.Building/src/BooleanMode.cs
Normal 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
|
||||||
|
}
|
86
VpSharp.Building/src/Command.cs
Normal file
86
VpSharp.Building/src/Command.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
35
VpSharp.Building/src/CommandAttribute.cs
Normal file
35
VpSharp.Building/src/CommandAttribute.cs
Normal 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; }
|
||||||
|
}
|
29
VpSharp.Building/src/Extensions/TextReaderExtensions.cs
Normal file
29
VpSharp.Building/src/Extensions/TextReaderExtensions.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
46
VpSharp.Building/src/FlagAttribute.cs
Normal file
46
VpSharp.Building/src/FlagAttribute.cs
Normal 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; }
|
||||||
|
}
|
45
VpSharp.Building/src/LightEffect.cs
Normal file
45
VpSharp.Building/src/LightEffect.cs
Normal 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
|
||||||
|
}
|
19
VpSharp.Building/src/LightType.cs
Normal file
19
VpSharp.Building/src/LightType.cs
Normal 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
|
||||||
|
}
|
29
VpSharp.Building/src/ParameterAttribute.cs
Normal file
29
VpSharp.Building/src/ParameterAttribute.cs
Normal 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; }
|
||||||
|
}
|
35
VpSharp.Building/src/PropertyAttribute.cs
Normal file
35
VpSharp.Building/src/PropertyAttribute.cs
Normal 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; }
|
||||||
|
}
|
491
VpSharp.Building/src/Serialization/ActionSerializer.Read.cs
Normal file
491
VpSharp.Building/src/Serialization/ActionSerializer.Read.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
367
VpSharp.Building/src/Serialization/ActionSerializer.Write.cs
Normal file
367
VpSharp.Building/src/Serialization/ActionSerializer.Write.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
39
VpSharp.Building/src/Serialization/ActionSerializer.cs
Normal file
39
VpSharp.Building/src/Serialization/ActionSerializer.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
132
VpSharp.Building/src/Serialization/CommandConverter.cs
Normal file
132
VpSharp.Building/src/Serialization/CommandConverter.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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))
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
@ -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; }
|
||||||
|
}
|
57
VpSharp.Building/src/Serialization/TokenType.cs
Normal file
57
VpSharp.Building/src/Serialization/TokenType.cs
Normal 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
|
||||||
|
}
|
116
VpSharp.Building/src/Serialization/Utf8ActionWriter.cs
Normal file
116
VpSharp.Building/src/Serialization/Utf8ActionWriter.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
55
VpSharp.Building/src/Serialization/ValueConverter.Generic.cs
Normal file
55
VpSharp.Building/src/Serialization/ValueConverter.Generic.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
53
VpSharp.Building/src/Serialization/ValueConverter.cs
Normal file
53
VpSharp.Building/src/Serialization/ValueConverter.cs
Normal 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);
|
||||||
|
}
|
@ -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)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
@ -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; }
|
||||||
|
}
|
25
VpSharp.Building/src/TextAlignment.cs
Normal file
25
VpSharp.Building/src/TextAlignment.cs
Normal 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
|
||||||
|
}
|
55
VpSharp.Building/src/Trigger.cs
Normal file
55
VpSharp.Building/src/Trigger.cs
Normal 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; }
|
||||||
|
}
|
35
VpSharp.Building/src/TriggerAttribute.cs
Normal file
35
VpSharp.Building/src/TriggerAttribute.cs
Normal 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; }
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
23
VpSharp.Building/src/ValueConverters/ByteValueConverter.cs
Normal file
23
VpSharp.Building/src/ValueConverters/ByteValueConverter.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
21
VpSharp.Building/src/ValueConverters/CharValueConverter.cs
Normal file
21
VpSharp.Building/src/ValueConverters/CharValueConverter.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
38
VpSharp.Building/src/ValueConverters/ColorValueConverter.cs
Normal file
38
VpSharp.Building/src/ValueConverters/ColorValueConverter.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
23
VpSharp.Building/src/ValueConverters/DoubleValueConverter.cs
Normal file
23
VpSharp.Building/src/ValueConverters/DoubleValueConverter.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
23
VpSharp.Building/src/ValueConverters/Int16ValueConverter.cs
Normal file
23
VpSharp.Building/src/ValueConverters/Int16ValueConverter.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
23
VpSharp.Building/src/ValueConverters/Int32ValueConverter.cs
Normal file
23
VpSharp.Building/src/ValueConverters/Int32ValueConverter.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
23
VpSharp.Building/src/ValueConverters/Int64ValueConverter.cs
Normal file
23
VpSharp.Building/src/ValueConverters/Int64ValueConverter.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
24
VpSharp.Building/src/ValueConverters/SByteValueConverter.cs
Normal file
24
VpSharp.Building/src/ValueConverters/SByteValueConverter.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
23
VpSharp.Building/src/ValueConverters/SingleValueConverter.cs
Normal file
23
VpSharp.Building/src/ValueConverters/SingleValueConverter.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
77
VpSharp.Building/src/ValueConverters/StringValueConverter.cs
Normal file
77
VpSharp.Building/src/ValueConverters/StringValueConverter.cs
Normal 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('"');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
24
VpSharp.Building/src/ValueConverters/UInt16ValueConverter.cs
Normal file
24
VpSharp.Building/src/ValueConverters/UInt16ValueConverter.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
24
VpSharp.Building/src/ValueConverters/UInt32ValueConverter.cs
Normal file
24
VpSharp.Building/src/ValueConverters/UInt32ValueConverter.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
24
VpSharp.Building/src/ValueConverters/UInt64ValueConverter.cs
Normal file
24
VpSharp.Building/src/ValueConverters/UInt64ValueConverter.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
171
VpSharp.Building/src/VirtualParadiseAction.cs
Normal file
171
VpSharp.Building/src/VirtualParadiseAction.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
119
VpSharp.Building/src/VirtualParadiseActionBuilder.cs
Normal file
119
VpSharp.Building/src/VirtualParadiseActionBuilder.cs
Normal 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] };
|
||||||
|
}
|
||||||
|
}
|
308
VpSharp.Building/src/VirtualParadiseColors.cs
Normal file
308
VpSharp.Building/src/VirtualParadiseColors.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
12
VpSharp.sln
12
VpSharp.sln
@ -32,6 +32,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Workflows", "Workflows", "{
|
|||||||
.github\workflows\release.yml = .github\workflows\release.yml
|
.github\workflows\release.yml = .github\workflows\release.yml
|
||||||
EndProjectSection
|
EndProjectSection
|
||||||
EndProject
|
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
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
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}.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.ActiveCfg = Release|Any CPU
|
||||||
{EF3BBA1B-8E9B-49A7-87C0-BE7D99E86B8F}.Release|Any CPU.Build.0 = 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
|
EndGlobalSection
|
||||||
EndGlobal
|
EndGlobal
|
||||||
|
Loading…
Reference in New Issue
Block a user