A Core/NosSmooth.PacketSerializersGenerator/AttributeGenerators/PacketConditionalIndexAttributeGenerator.cs => Core/NosSmooth.PacketSerializersGenerator/AttributeGenerators/PacketConditionalIndexAttributeGenerator.cs +299 -0
@@ 0,0 1,299 @@
+//
+// PacketConditionalIndexAttributeGenerator.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.CodeDom.Compiler;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using NosSmooth.PacketSerializersGenerator.Data;
+using NosSmooth.PacketSerializersGenerator.Errors;
+using NosSmooth.PacketSerializersGenerator.Extensions;
+
+namespace NosSmooth.PacketSerializersGenerator.AttributeGenerators;
+
+/// <inheritdoc />
+public class PacketConditionalIndexAttributeGenerator : IParameterGenerator
+{
+ /// <summary>
+ /// Gets the full name of the packet index attribute.
+ /// </summary>
+ public static string PacketConditionalIndexAttributeFullName
+ => "NosSmooth.Packets.Attributes.PacketConditionalIndexAttribute";
+
+ /// <inheritdoc />
+ public bool ShouldHandle(ParameterInfo parameter)
+ => parameter.Attributes.Any(x => x.FullName == PacketConditionalIndexAttributeFullName);
+
+ /// <inheritdoc />
+ public IError? CheckParameter(PacketInfo packet, ParameterInfo parameter)
+ {
+ if (!parameter.Nullable)
+ {
+ return new DiagnosticError
+ (
+ "SGNull",
+ "Conditional parameters must be nullable",
+ "The parameter {0} in {1} has to be nullable, because it is conditional.",
+ parameter.Parameter.SyntaxTree,
+ parameter.Parameter.FullSpan,
+ new List<object?>(new[] { parameter.Name, packet.Name })
+ );
+ }
+
+ if (parameter.Attributes.Any(x => x.FullName != PacketConditionalIndexAttributeFullName))
+ {
+ return new DiagnosticError
+ (
+ "SGAttr",
+ "Packet constructor parameter with multiple packet attributes",
+ "Found multiple packet attributes of multiple types on parameter {0} in {1}. PacketConditionalIndexAttribute supports multiple attributes of the same type only.",
+ parameter.Parameter.SyntaxTree,
+ parameter.Parameter.FullSpan,
+ new List<object?>
+ (
+ new[]
+ {
+ parameter.Name,
+ packet.Name
+ }
+ )
+ );
+ }
+
+ // Check that all attributes have the same data. (where the same data need to be)
+ var firstAttribute = parameter.Attributes.First();
+ if (parameter.Attributes.Any
+ (
+ x =>
+ {
+ var index = x.GetIndexedValue<int>(0);
+ if (index != parameter.PacketIndex)
+ {
+ return true;
+ }
+
+ foreach (var keyValue in x.NamedAttributeArguments)
+ {
+ if (!firstAttribute.NamedAttributeArguments.ContainsKey(keyValue.Key))
+ {
+ return true;
+ }
+
+ if (firstAttribute.NamedAttributeArguments[keyValue.Key] != keyValue.Value)
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+ ))
+ {
+ return new DiagnosticError
+ (
+ "SGAttr",
+ "Packet constructor parameter with multiple conflicting attribute data.",
+ "Found multiple packet attributes of multiple types on parameter {0} in {1} with conflicting data. Index, IsOptional, InnerSeparator, AfterSeparator all have to be the same for each attribute.",
+ parameter.Parameter.SyntaxTree,
+ parameter.Parameter.FullSpan,
+ new List<object?>
+ (
+ new[]
+ {
+ parameter.Name,
+ packet.Name
+ }
+ )
+ );
+ }
+
+ var mismatchedAttribute = parameter.Attributes.FirstOrDefault
+ (
+ x => x.IndexedAttributeArguments.Count < 4 ||
+ (x.IndexedAttributeArguments[3].IsArray &&
+ (
+ (x.IndexedAttributeArguments[3].Argument.Expression as ArrayCreationExpressionSyntax)
+ ?.Initializer is null ||
+ (x.IndexedAttributeArguments[3].Argument.Expression as ArrayCreationExpressionSyntax)
+ ?.Initializer?.Expressions.Count == 0
+ )
+ )
+ );
+ if (mismatchedAttribute is not null)
+ {
+ return new DiagnosticError
+ (
+ "SGAttr",
+ "Packet conditional attribute without matching values",
+ "Found PacketConditionalIndexAttribute without matchValues parameters set on {0} in {1}. At least one parameter has to be specified.",
+ mismatchedAttribute.Attribute.SyntaxTree,
+ mismatchedAttribute.Attribute.FullSpan,
+ new List<object?>
+ (
+ new[]
+ {
+ parameter.Name,
+ packet.Name
+ }
+ )
+ );
+ }
+
+ return ParameterChecker.CheckOptionalIsNullable(packet, parameter);
+ }
+
+ private string BuildAttributeIfPart(AttributeInfo attribute, string prefix)
+ {
+ var conditionParameterName = attribute.GetIndexedValue<string>(1);
+ var negate = attribute.GetIndexedValue<bool>(2);
+ var values = attribute.GetParamsVisualValues(3);
+ if (conditionParameterName is null || values is null)
+ {
+ throw new ArgumentException();
+ }
+
+ var inner = string.Join
+ (" || ", values.Select(x => $"{prefix}{conditionParameterName.Trim('"')} == {x?.ToString() ?? "null"}"));
+ return (negate ? "!(" : string.Empty) + inner + (negate ? ")" : string.Empty);
+ }
+
+ private string BuildParameterIfStatement(ParameterInfo parameter, string prefix)
+ {
+ var ifInside = string.Empty;
+ foreach (var attribute in parameter.Attributes)
+ {
+ ifInside += BuildAttributeIfPart(attribute, prefix);
+ }
+
+ return $"if ({ifInside})";
+ }
+
+ /// <inheritdoc />
+ public IError? GenerateSerializerPart(IndentedTextWriter textWriter, PacketInfo packetInfo)
+ {
+ bool pushedLevel = false;
+ var generator = new ConverterSerializationGenerator(textWriter);
+ var parameter = packetInfo.Parameters.Current;
+ var attributes = parameter.Attributes;
+ var attribute = attributes.First();
+
+ // begin conditional if
+ textWriter.WriteLine(BuildParameterIfStatement(parameter, "obj."));
+ textWriter.WriteLine("{");
+ textWriter.Indent++;
+
+ if (parameter.IsOptional())
+ {
+ textWriter.WriteLine($"if (obj.{parameter.GetVariableName()} is not null)");
+ textWriter.WriteLine("{");
+ textWriter.Indent++;
+ }
+
+ // register after separator
+ var afterSeparator = attribute.GetNamedValue<char?>("AfterSeparator", null);
+ if (afterSeparator is not null)
+ {
+ generator.SetAfterSeparatorOnce((char)afterSeparator);
+ }
+
+ // push inner separator level
+ var innerSeparator = attribute.GetNamedValue<char?>("InnerSeparator", null);
+ if (innerSeparator is not null)
+ {
+ generator.PushLevel((char)innerSeparator);
+ pushedLevel = true;
+ }
+
+ // serialize, check the error.
+ generator.SerializeAndCheck(parameter);
+
+ // pop inner separator level
+ if (pushedLevel)
+ {
+ generator.PopLevel();
+ }
+
+ // end optional if
+ if (parameter.IsOptional())
+ {
+ textWriter.Indent--;
+ textWriter.WriteLine("}");
+ }
+
+ // end conditional if
+ textWriter.Indent--;
+ textWriter.WriteLine("}");
+
+ return null;
+ }
+
+ /// <inheritdoc />
+ public IError? GenerateDeserializerPart(IndentedTextWriter textWriter, PacketInfo packetInfo)
+ {
+ bool pushedLevel = false;
+ var generator = new ConverterDeserializationGenerator(textWriter);
+ var parameter = packetInfo.Parameters.Current;
+ var attribute = parameter.Attributes.First();
+
+ generator.DeclareLocalVariable(parameter);
+
+ // begin conditional if
+ textWriter.WriteLine(BuildParameterIfStatement(parameter, string.Empty));
+ textWriter.WriteLine("{");
+ textWriter.Indent++;
+
+ // add optional if
+ if (parameter.IsOptional())
+ {
+ generator.StartOptionalCheck(parameter, packetInfo.Name);
+ }
+
+ var afterSeparator = attribute.GetNamedValue<char?>("AfterSeparator", null);
+ if (afterSeparator is not null)
+ {
+ generator.SetAfterSeparatorOnce((char)afterSeparator);
+ }
+
+ var innerSeparator = attribute.GetNamedValue<char?>("InnerSeparator", null);
+ if (innerSeparator is not null)
+ {
+ generator.PushLevel((char)innerSeparator);
+ pushedLevel = true;
+ }
+
+ generator.DeserializeAndCheck
+ ($"{packetInfo.Namespace}.{packetInfo.Name}", parameter, packetInfo.Parameters.IsLast);
+
+ if (!parameter.Nullable)
+ {
+ generator.CheckNullError(parameter.GetResultVariableName(), parameter.Name);
+ }
+
+ generator.AssignLocalVariable(parameter, false);
+ if (pushedLevel)
+ {
+ generator.ReadToLastToken();
+ generator.PopLevel();
+ }
+
+ // end is last token if body
+ if (parameter.IsOptional())
+ {
+ generator.EndOptionalCheck(parameter);
+ }
+
+ // end conditional if
+ textWriter.Indent--;
+ textWriter.WriteLine("}");
+ textWriter.WriteLine("else");
+ textWriter.WriteLine("{");
+ textWriter.Indent++;
+ textWriter.WriteLine($"{parameter.GetVariableName()} = null;");
+ textWriter.Indent--;
+ textWriter.WriteLine("}");
+
+ return null;
+ }
+}<
\ No newline at end of file
M Core/NosSmooth.PacketSerializersGenerator/Data/AttributeInfo.cs => Core/NosSmooth.PacketSerializersGenerator/Data/AttributeInfo.cs +12 -3
@@ 19,6 19,15 @@ public record AttributeInfo
(
AttributeSyntax Attribute,
string FullName,
- IReadOnlyList<object?> IndexedAttributeArguments,
- IReadOnlyDictionary<string, object?> NamedAttributeArguments
-);>
\ No newline at end of file
+ IReadOnlyList<AttributeArgumentInfo> IndexedAttributeArguments,
+ IReadOnlyDictionary<string, AttributeArgumentInfo> NamedAttributeArguments
+);
+
+/// <summary>
+/// The attribute argument information.
+/// </summary>
+/// <param name="Argument">The argument syntax.</param>
+/// <param name="IsArray">Whether the attribute argument is an array.</param>
+/// <param name="RealValue">The real parsed value of the argument.</param>
+/// <param name="VisualValue">The visual value of the argument.</param>
+public record AttributeArgumentInfo(AttributeArgumentSyntax Argument, bool IsArray, object? RealValue, string VisualValue);<
\ No newline at end of file
M Core/NosSmooth.PacketSerializersGenerator/Data/Parameters.cs => Core/NosSmooth.PacketSerializersGenerator/Data/Parameters.cs +2 -1
@@ 4,6 4,7 @@
// Copyright (c) František Boháček. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+using NosSmooth.PacketSerializersGenerator.AttributeGenerators;
using NosSmooth.PacketSerializersGenerator.Extensions;
namespace NosSmooth.PacketSerializersGenerator.Data;
@@ 61,7 62,7 @@ public class Parameters
{
for (int i = CurrentIndex + 1; i < List.Count; i++)
{
- if (!List[i].IsOptional())
+ if (!List[i].IsOptional() && List[i].Attributes.All(x => x.FullName != PacketConditionalIndexAttributeGenerator.PacketConditionalIndexAttributeFullName))
{
return false;
}
M Core/NosSmooth.PacketSerializersGenerator/Extensions/AttributeInfoExtensions.cs => Core/NosSmooth.PacketSerializersGenerator/Extensions/AttributeInfoExtensions.cs +52 -4
@@ 5,6 5,7 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
using System.Diagnostics;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
using NosSmooth.PacketSerializersGenerator.Data;
namespace NosSmooth.PacketSerializersGenerator.Extensions;
@@ 28,10 29,10 @@ public static class AttributeInfoExtensions
{
if (typeof(TValue) == typeof(string))
{
- return (TValue?)(object?)value?.ToString();
+ return (TValue?)(object?)value.VisualValue;
}
- return (TValue?)value;
+ return (TValue?)value.RealValue;
}
return @default;
@@ 50,9 51,56 @@ public static class AttributeInfoExtensions
if (typeof(TValue) == typeof(string))
{
- return (TValue?)(object?)value?.ToString();
+ return (TValue?)(object?)value?.VisualValue;
}
- return (TValue?)value;
+ return (TValue?)value.RealValue;
+ }
+
+ /// <summary>
+ /// Gets visual values of params parameters in the constructor.
+ /// </summary>
+ /// <param name="attributeInfo">The attribute info.</param>
+ /// <param name="startingIndex">The index the values start at.</param>
+ /// <returns>A list containing all the values.</returns>
+ public static IReadOnlyList<string> GetParamsVisualValues(this AttributeInfo attributeInfo, int startingIndex)
+ {
+ if (attributeInfo.IndexedAttributeArguments.Count - 1 < startingIndex)
+ {
+ return Array.Empty<string>();
+ }
+
+ if (attributeInfo.IndexedAttributeArguments[startingIndex].IsArray)
+ {
+ return attributeInfo.IndexedAttributeArguments[startingIndex].GetArrayVisualValues();
+ }
+
+ return attributeInfo.IndexedAttributeArguments
+ .Skip(startingIndex)
+ .Select(x => x.VisualValue)
+ .ToArray();
+ }
+
+ /// <summary>
+ /// Gets the visual values of the array.
+ /// </summary>
+ /// <param name="attributeArgumentInfo">The attribute argument.</param>
+ /// <returns>The list of the elements.</returns>
+ public static IReadOnlyList<string> GetArrayVisualValues(this AttributeArgumentInfo attributeArgumentInfo)
+ {
+ var arrayCreation = attributeArgumentInfo.Argument.Expression as ArrayCreationExpressionSyntax;
+ if (arrayCreation is null)
+ {
+ throw new ArgumentException($"The given attribute argument is not an array creation.", nameof(attributeArgumentInfo));
+ }
+
+ if (arrayCreation.Initializer is null)
+ {
+ return Array.Empty<string>();
+ }
+
+ return arrayCreation.Initializer.Expressions
+ .Select(x => x.ToString())
+ .ToArray();
}
}=
\ No newline at end of file
M Core/NosSmooth.PacketSerializersGenerator/PacketConverterGenerator.cs => Core/NosSmooth.PacketSerializersGenerator/PacketConverterGenerator.cs +6 -0
@@ 5,6 5,7 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
using System.CodeDom.Compiler;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
using NosSmooth.PacketSerializersGenerator.AttributeGenerators;
using NosSmooth.PacketSerializersGenerator.Data;
using NosSmooth.PacketSerializersGenerator.Errors;
@@ 38,6 39,10 @@ public class PacketConverterGenerator
/// <returns>An error, if any.</returns>
public IError? Generate(IndentedTextWriter textWriter)
{
+ var usings = _packetInfo.PacketRecord.SyntaxTree.GetRoot()
+ .DescendantNodes()
+ .OfType<UsingDirectiveSyntax>();
+ var usingsString = string.Join("\n", usings.Select(x => x.ToString()));
textWriter.WriteLine
(
@$"// <auto-generated/>
@@ 49,6 54,7 @@ using NosSmooth.Packets.Converters;
using NosSmooth.Packets.Errors;
using NosSmooth.Packets;
using Remora.Results;
+{usingsString}
namespace {_packetInfo.Namespace}.Generated;
M Core/NosSmooth.PacketSerializersGenerator/SourceGenerator.cs => Core/NosSmooth.PacketSerializersGenerator/SourceGenerator.cs +8 -4
@@ 36,8 36,10 @@ public class SourceGenerator : ISourceGenerator
new IParameterGenerator[]
{
new PacketIndexAttributeGenerator(),
+ new PacketGreedyIndexAttributeGenerator(),
new PacketListIndexAttributeGenerator(),
new PacketContextListAttributeGenerator(),
+ new PacketConditionalIndexAttributeGenerator(),
}
);
}
@@ 47,6 49,7 @@ public class SourceGenerator : ISourceGenerator
/// <inheritdoc />
public void Initialize(GeneratorInitializationContext context)
{
+ // SpinWait.SpinUntil(() => Debugger.IsAttached);
}
private IEnumerable<RecordDeclarationSyntax> GetPacketRecords(Compilation compilation, SyntaxTree tree)
@@ 257,21 260,22 @@ public class SourceGenerator : ISourceGenerator
private AttributeInfo CreateAttributeInfo(AttributeSyntax attribute, SemanticModel semanticModel)
{
- var namedArguments = new Dictionary<string, object?>();
- var arguments = new List<object?>();
+ var namedArguments = new Dictionary<string, AttributeArgumentInfo>();
+ var arguments = new List<AttributeArgumentInfo>();
foreach (var argument in attribute.ArgumentList!.Arguments)
{
var argumentName = argument.NameEquals?.Name.Identifier.NormalizeWhitespace().ToFullString();
var value = argument.GetValue(semanticModel);
+ bool isArray = argument.Expression is ArrayCreationExpressionSyntax;
if (argumentName is not null)
{
- namedArguments.Add(argumentName, value);
+ namedArguments.Add(argumentName, new AttributeArgumentInfo(argument, isArray, value, argument.ToString()));
}
else
{
- arguments.Add(value);
+ arguments.Add(new AttributeArgumentInfo(argument, isArray, value, argument.ToString()));
}
}
A Core/NosSmooth.Packets/Attributes/PacketConditionalIndexAttribute.cs => Core/NosSmooth.Packets/Attributes/PacketConditionalIndexAttribute.cs +26 -0
@@ 0,0 1,26 @@
+//
+// PacketConditionalIndexAttribute.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+namespace NosSmooth.Packets.Attributes;
+
+/// <summary>
+/// Attribute for marking packet properties that may appear only in specific conditions.
+/// </summary>
+public class PacketConditionalIndexAttribute : PacketIndexAttribute
+{
+ /// <summary>
+ /// Initializes a new instance of the <see cref="PacketConditionalIndexAttribute"/> class.
+ /// You can use this attribute multiple times on one parameter.
+ /// </summary>
+ /// <param name="index">The position in the packet.</param>
+ /// <param name="conditionParameter">What parameter to check. (it has to precede this one).</param>
+ /// <param name="negate">Whether to negate the match values (not equals).</param>
+ /// <param name="matchValues">The values that mean this parameter is present.</param>
+ public PacketConditionalIndexAttribute(ushort index, string conditionParameter, bool negate = false, params object?[] matchValues)
+ : base(index)
+ {
+ }
+}<
\ No newline at end of file
A Core/NosSmooth.Packets/Common/NameString.cs => Core/NosSmooth.Packets/Common/NameString.cs +103 -0
@@ 0,0 1,103 @@
+//
+// NameString.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.Net.NetworkInformation;
+using System.Reflection;
+
+namespace NosSmooth.Packets.Common;
+
+/// <summary>
+/// Represents name in the game replacing "^" for " ".
+/// </summary>
+public class NameString
+{
+ /// <summary>
+ /// Gets the character used to separate words.
+ /// </summary>
+ public static char WordSeparator => '^';
+
+ /// <summary>
+ /// Creates <see cref="NameString"/> instance from the given name retrieved from a packet.
+ /// </summary>
+ /// <param name="packetName">The name from the packet.</param>
+ /// <returns>A name string instance.</returns>
+ public static NameString FromPacket(string packetName)
+ {
+ return new NameString(packetName, true);
+ }
+
+ /// <summary>
+ /// Creates <see cref="NameString"/> instance from the given name retrieved from a packet.
+ /// </summary>
+ /// <param name="packetName">The name from the packet.</param>
+ /// <returns>A name string instance.</returns>
+ public static NameString FromString(string packetName)
+ {
+ return new NameString(packetName, true);
+ }
+
+ private NameString(string name, bool packet)
+ {
+ if (packet)
+ {
+ PacketName = name;
+ Name = name.Replace(WordSeparator, ' ');
+ }
+ else
+ {
+ Name = name;
+ PacketName = name.Replace(' ', WordSeparator);
+ }
+ }
+
+ /// <summary>
+ /// The real name.
+ /// </summary>
+ public string Name { get; }
+
+ /// <summary>
+ /// The original name in the packet.
+ /// </summary>
+ public string PacketName { get; }
+
+ /// <inheritdoc />
+ public override string ToString()
+ {
+ return Name;
+ }
+
+ /// <summary>
+ /// Converts name string to regular string.
+ /// Returns the real name.
+ /// </summary>
+ /// <param name="nameString">The name string to convert.</param>
+ /// <returns>The real name.</returns>
+ public static implicit operator string(NameString nameString)
+ {
+ return nameString.Name;
+ }
+
+ /// <summary>
+ /// Converts regular string to name string.
+ /// </summary>
+ /// <param name="name">The string to convert.</param>
+ /// <returns>The name string.</returns>
+ public static implicit operator NameString(string name)
+ {
+ return FromString(name);
+ }
+
+ /// <inheritdoc />
+ public override bool Equals(object? obj)
+ {
+ if (!(obj is NameString nameString))
+ {
+ return false;
+ }
+
+ return Name.Equals(nameString.Name);
+ }
+}<
\ No newline at end of file
A Core/NosSmooth.Packets/Converters/Common/NameStringConverter.cs => Core/NosSmooth.Packets/Converters/Common/NameStringConverter.cs +42 -0
@@ 0,0 1,42 @@
+//
+// NameStringConverter.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using NosSmooth.Packets.Common;
+using NosSmooth.Packets.Converters.Basic;
+using Remora.Results;
+
+namespace NosSmooth.Packets.Converters.Common;
+
+/// <summary>
+/// Converter of <see cref="NameString"/>.
+/// </summary>
+public class NameStringConverter : BaseTypeConverter<NameString>
+{
+ /// <inheritdoc />
+ public override Result Serialize(NameString? obj, PacketStringBuilder builder)
+ {
+ if (obj is null)
+ {
+ builder.Append("-");
+ return Result.FromSuccess();
+ }
+
+ builder.Append(obj.PacketName);
+ return Result.FromSuccess();
+ }
+
+ /// <inheritdoc />
+ public override Result<NameString?> Deserialize(PacketStringEnumerator stringEnumerator)
+ {
+ var tokenResult = stringEnumerator.GetNextToken();
+ if (!tokenResult.IsSuccess)
+ {
+ return Result<NameString?>.FromError(tokenResult);
+ }
+
+ return NameString.FromPacket(tokenResult.Entity.Token);
+ }
+}<
\ No newline at end of file
M Core/NosSmooth.Packets/Extensions/ServiceCollectionExtensions.cs => Core/NosSmooth.Packets/Extensions/ServiceCollectionExtensions.cs +2 -0
@@ 10,6 10,7 @@ using System.Reflection;
using Microsoft.Extensions.DependencyInjection;
using NosSmooth.Packets.Converters;
using NosSmooth.Packets.Converters.Basic;
+using NosSmooth.Packets.Converters.Common;
using NosSmooth.Packets.Converters.Special;
using NosSmooth.Packets.Packets;
@@ 98,6 99,7 @@ public static class ServiceCollectionExtensions
.AddTypeConverter<ULongTypeConverter>()
.AddTypeConverter<LongTypeConverter>()
.AddTypeConverter<StringTypeConverter>()
+ .AddTypeConverter<NameStringConverter>()
.AddTypeConverter<CharTypeConverter>();
}