diff --git a/DisplayCommands/DisplayCommands.csproj b/DisplayCommands/DisplayCommands.csproj
index f621f74..eb5123d 100644
--- a/DisplayCommands/DisplayCommands.csproj
+++ b/DisplayCommands/DisplayCommands.csproj
@@ -22,6 +22,10 @@
+
+
diff --git a/DisplayCommands/Internals/DisplayConnection.cs b/DisplayCommands/Internals/DisplayConnection.cs
index 4505d95..972a5db 100644
--- a/DisplayCommands/Internals/DisplayConnection.cs
+++ b/DisplayCommands/Internals/DisplayConnection.cs
@@ -14,7 +14,7 @@ internal sealed class DisplayConnection(IOptions options)
public ValueTask SendClearAsync()
{
- var header = new HeaderWindow { Command = DisplayCommand.Clear };
+ var header = new HeaderWindow { Command = (ushort)DisplayCommand.Clear };
return SendAsync(header, Memory.Empty);
}
@@ -23,7 +23,7 @@ internal sealed class DisplayConnection(IOptions options)
{
var header = new HeaderWindow
{
- Command = DisplayCommand.Cp437Data,
+ Command = (ushort)DisplayCommand.Cp437Data,
Height = grid.Height,
Width = grid.Width,
PosX = x,
@@ -37,7 +37,7 @@ internal sealed class DisplayConnection(IOptions options)
{
var header = new HeaderWindow
{
- Command = DisplayCommand.CharBrightness,
+ Command = (ushort)DisplayCommand.CharBrightness,
PosX = x,
PosY = y,
Height = luma.Height,
@@ -49,7 +49,7 @@ internal sealed class DisplayConnection(IOptions options)
public async ValueTask SendBrightnessAsync(byte brightness)
{
- var header = new HeaderWindow { Command = DisplayCommand.Brightness };
+ var header = new HeaderWindow { Command = (ushort)DisplayCommand.Brightness };
var payloadBuffer = _arrayPool.Rent(1);
var payload = payloadBuffer.AsMemory(0, 1);
@@ -61,13 +61,13 @@ internal sealed class DisplayConnection(IOptions options)
public ValueTask SendHardResetAsync()
{
- var header = new HeaderWindow { Command = DisplayCommand.HardReset };
+ var header = new HeaderWindow { Command = (ushort)DisplayCommand.HardReset };
return SendAsync(header, Memory.Empty);
}
public async ValueTask SendFadeOutAsync(byte loops)
{
- var header = new HeaderWindow { Command = DisplayCommand.FadeOut };
+ var header = new HeaderWindow { Command = (ushort)DisplayCommand.FadeOut };
var payloadBuffer = _arrayPool.Rent(1);
var payload = payloadBuffer.AsMemory(0, 1);
@@ -81,8 +81,9 @@ internal sealed class DisplayConnection(IOptions options)
{
var header = new HeaderWindow
{
- Command = DisplayCommand.BitmapLinearWin,
- PosX = x, PosY = y,
+ Command = (ushort)DisplayCommand.BitmapLinearWin,
+ PosX = x,
+ PosY = y,
Width = (ushort)(pixels.Width / 8),
Height = pixels.Height
};
@@ -113,7 +114,6 @@ internal sealed class DisplayConnection(IOptions options)
var buffer = _arrayPool.Rent(messageSize);
var message = buffer.AsMemory(0, messageSize);
- header.ChangeToNetworkOrder();
MemoryMarshal.Write(message.Span, header);
payload.CopyTo(message[headerSize..]);
diff --git a/DisplayCommands/Internals/HeaderBitmap.cs b/DisplayCommands/Internals/HeaderBitmap.cs
index e3bdd43..b733995 100644
--- a/DisplayCommands/Internals/HeaderBitmap.cs
+++ b/DisplayCommands/Internals/HeaderBitmap.cs
@@ -1,17 +1,19 @@
using System.Runtime.InteropServices;
+using EndiannessSourceGenerator;
namespace DisplayCommands.Internals;
+[StructEndianness(IsLittleEndian = false)]
[StructLayout(LayoutKind.Sequential, Pack = 16, Size = 10)]
-internal struct HeaderBitmap
+internal partial struct HeaderBitmap
{
- public DisplayCommand Command;
+ private ushort _command;
- public ushort Offset;
+ private ushort _offset;
- public ushort Length;
+ private ushort _length;
- public DisplaySubCommand SubCommand;
+ private ushort _subCommand;
- public ushort Reserved;
-}
\ No newline at end of file
+ private ushort _reserved;
+}
diff --git a/DisplayCommands/Internals/HeaderWindow.cs b/DisplayCommands/Internals/HeaderWindow.cs
index fcb9176..2be4916 100644
--- a/DisplayCommands/Internals/HeaderWindow.cs
+++ b/DisplayCommands/Internals/HeaderWindow.cs
@@ -1,29 +1,19 @@
-using System.Buffers.Binary;
using System.Runtime.InteropServices;
+using EndiannessSourceGenerator;
namespace DisplayCommands.Internals;
+[StructEndianness(IsLittleEndian = false)]
[StructLayout(LayoutKind.Sequential, Pack = 16, Size = 10)]
-internal struct HeaderWindow
+internal partial struct HeaderWindow
{
- public DisplayCommand Command;
+ private ushort _command;
- public ushort PosX;
+ private ushort _posX;
- public ushort PosY;
+ private ushort _posY;
- public ushort Width;
+ private ushort _width;
- public ushort Height;
-
- public void ChangeToNetworkOrder()
- {
- if (!BitConverter.IsLittleEndian)
- return;
- Command = (DisplayCommand)BinaryPrimitives.ReverseEndianness((ushort)Command);
- PosX = BinaryPrimitives.ReverseEndianness(PosX);
- PosY = BinaryPrimitives.ReverseEndianness(PosY);
- Width = BinaryPrimitives.ReverseEndianness(Width);
- Height = BinaryPrimitives.ReverseEndianness(Height);
- }
+ private ushort _height;
}
diff --git a/EndiannessSourceGenerator/EndiannessGenerator.cs b/EndiannessSourceGenerator/EndiannessGenerator.cs
new file mode 100644
index 0000000..65a4be8
--- /dev/null
+++ b/EndiannessSourceGenerator/EndiannessGenerator.cs
@@ -0,0 +1,227 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Microsoft.CodeAnalysis.Text;
+
+namespace EndiannessSourceGenerator;
+
+internal class DebugMeException(string message) : Exception(message);
+
+internal class InvalidUsageException(string message) : Exception(message);
+
+[Generator]
+public class StructEndiannessSourceGenerator : ISourceGenerator
+{
+ private const string Namespace = "EndiannessSourceGenerator";
+ private const string AttributeName = "StructEndiannessAttribute";
+ private const string IsLittleEndianProperty = "IsLittleEndian";
+
+ private const string AttributeSourceCode =
+ $$"""
+ //
+ namespace {{Namespace}}
+ {
+ [System.AttributeUsage(System.AttributeTargets.Struct)]
+ public class {{AttributeName}}: System.Attribute
+ {
+ public required bool {{IsLittleEndianProperty}} { get; init; }
+ }
+ }
+ """;
+
+ private const string UsingDeclarations =
+ """
+ using System;
+ using System.Buffers.Binary;
+ """;
+
+ public void Initialize(GeneratorInitializationContext context)
+ {
+ // Register the attribute source
+ context.RegisterForPostInitialization(i => { i.AddSource($"{AttributeName}.g.cs", AttributeSourceCode); });
+ // context.RegisterForSyntaxNotifications(() => new SyntaxCon);
+ }
+
+ private readonly SymbolDisplayFormat _namespacedNameFormat =
+ new(typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces);
+
+ // TODO: generate syntax tree with roslyn to get rid of string wrangling and so code is properly formatted
+ public void Execute(GeneratorExecutionContext context)
+ {
+ var treesWithStructsWithAttributes = context.Compilation.SyntaxTrees
+ .Where(st => st.GetRoot().DescendantNodes()
+ .OfType()
+ .Any(p => p.DescendantNodes()
+ .OfType()
+ .Any()))
+ .ToList();
+
+ foreach (var tree in treesWithStructsWithAttributes)
+ {
+ var semanticModel = context.Compilation.GetSemanticModel(tree);
+
+ var structsWithAttributes = tree.GetRoot().DescendantNodes()
+ .OfType()
+ .Where(cd => cd.DescendantNodes()
+ .OfType()
+ .Any());
+
+ foreach (var structDeclaration in structsWithAttributes)
+ {
+ var foundAttribute = GetEndiannessAttribute(structDeclaration, semanticModel);
+ // not my type
+ if (foundAttribute == null)
+ continue;
+
+ HandleStruct(context, structDeclaration, semanticModel, foundAttribute);
+ }
+ }
+ }
+
+ private static void HandleStruct(GeneratorExecutionContext context, StructDeclarationSyntax structDeclaration,
+ SemanticModel semanticModel, AttributeSyntax foundAttribute)
+ {
+ var isPartial = structDeclaration.Modifiers.Any(m => m.IsKind(SyntaxKind.PartialKeyword));
+ if (!isPartial)
+ throw new InvalidUsageException("struct is not marked partial");
+
+ var accessibilityModifier = structDeclaration.Modifiers.Any(m => m.IsKind(SyntaxKind.InternalKeyword))
+ ? "internal"
+ : "public";
+
+ var structType = semanticModel.GetDeclaredSymbol(structDeclaration);
+ if (structType == null)
+ throw new DebugMeException("struct type info is null");
+
+ var structNamespace = structType.ContainingNamespace?.ToDisplayString();
+ if (structNamespace == null)
+ throw new InvalidUsageException("struct has to be contained in a namespace");
+
+ var structIsLittleEndian = GetStructIsLittleEndian(foundAttribute);
+
+ var generatedCode = new StringBuilder();
+ generatedCode.AppendLine(UsingDeclarations);
+
+ generatedCode.AppendLine($"namespace {structNamespace};");
+ generatedCode.AppendLine($$"""{{accessibilityModifier}} partial struct {{structType.Name}} {""");
+
+ var hasProperties = structDeclaration.Members
+ .Any(m => m.IsKind(SyntaxKind.PropertyDeclaration));
+ if (hasProperties)
+ throw new InvalidUsageException("struct cannot have properties");
+
+ var fieldDeclarations = structDeclaration.Members
+ .Where(m => m.IsKind(SyntaxKind.FieldDeclaration)).OfType();
+ GenerateStructProperties(generatedCode, fieldDeclarations, semanticModel, structIsLittleEndian);
+
+ generatedCode.AppendLine("}"); // end of struct
+
+ context.AddSource($"{structNamespace}.{structType.Name}.g.cs",
+ SourceText.From(generatedCode.ToString(), Encoding.UTF8));
+ }
+
+ private static void GenerateStructProperties(StringBuilder generatedCode,
+ IEnumerable fieldDeclarations, SemanticModel semanticModel, bool structIsLittleEndian)
+ {
+ foreach (var field in fieldDeclarations)
+ {
+ if (!field.Modifiers.Any(m => m.IsKind(SyntaxKind.PrivateKeyword)))
+ throw new InvalidUsageException("fields have to be private");
+
+ var variableDeclaration = field.DescendantNodes()
+ .OfType()
+ .FirstOrDefault();
+ if (variableDeclaration == null)
+ throw new DebugMeException("variable declaration of field declaration null");
+
+ var variableTypeInfo = semanticModel.GetTypeInfo(variableDeclaration.Type).Type;
+ if (variableTypeInfo == null)
+ throw new DebugMeException("variable type info of field declaration null");
+
+ var typeName = variableTypeInfo.ToDisplayString();
+ var fieldName = variableDeclaration.Variables.First().Identifier.ToString();
+ var propertyName = GeneratePropertyName(fieldName);
+
+ GenerateProperty(generatedCode, typeName, propertyName, structIsLittleEndian, fieldName);
+ }
+ }
+
+ private static void GenerateProperty(StringBuilder generatedCode, string typeName, string propertyName,
+ bool structIsLittleEndian, string fieldName)
+ {
+ generatedCode.AppendLine($$"""public {{typeName}} {{propertyName}} {""");
+
+ var maybeNegator = structIsLittleEndian ? string.Empty : "!";
+ var sameEndiannessExpression = $"{maybeNegator}BitConverter.IsLittleEndian";
+
+ generatedCode.AppendLine($"get => {sameEndiannessExpression}");
+ generatedCode.AppendLine($" ? {fieldName}");
+ generatedCode.AppendLine($" : BinaryPrimitives.ReverseEndianness({fieldName});");
+
+ generatedCode.AppendLine($"set => {fieldName} = {sameEndiannessExpression}");
+ generatedCode.AppendLine(" ? value");
+ generatedCode.AppendLine(" : BinaryPrimitives.ReverseEndianness(value);");
+
+ generatedCode.AppendLine("}"); // end of property
+ }
+
+ private static string GeneratePropertyName(string fieldName)
+ {
+ var propertyName = fieldName;
+ if (propertyName.StartsWith("_"))
+ propertyName = propertyName.Substring(1);
+ if (!char.IsLetter(propertyName, 0) || char.IsUpper(propertyName, 0))
+ throw new InvalidUsageException("field names have to start with a lower case letter");
+ propertyName = propertyName.Substring(0, 1).ToUpperInvariant()
+ + propertyName.Substring(1);
+ return propertyName;
+ }
+
+ private static AttributeSyntax? GetEndiannessAttribute(StructDeclarationSyntax structDeclaration,
+ SemanticModel semanticModel)
+ {
+ AttributeSyntax? foundAttribute = null;
+ foreach (var attributeSyntax in structDeclaration.DescendantNodes().OfType())
+ {
+ var attributeTypeInfo = semanticModel.GetTypeInfo(attributeSyntax).Type;
+ if (attributeTypeInfo == null)
+ throw new DebugMeException("attribute type info is null");
+
+ if (attributeTypeInfo.ContainingNamespace?.Name != Namespace)
+ continue;
+ if (attributeTypeInfo.Name != AttributeName)
+ continue;
+
+ foundAttribute = attributeSyntax;
+ break;
+ }
+
+ return foundAttribute;
+ }
+
+ private static bool GetStructIsLittleEndian(AttributeSyntax foundAttribute)
+ {
+ var endiannessArguments = foundAttribute.ArgumentList;
+ if (endiannessArguments == null)
+ throw new InvalidUsageException("endianness attribute has no arguments");
+
+ var isLittleEndianArgumentSyntax = endiannessArguments.Arguments
+ .FirstOrDefault(argumentSyntax =>
+ argumentSyntax.NameEquals?.Name.Identifier.ToString() == IsLittleEndianProperty);
+ if (isLittleEndianArgumentSyntax == null)
+ throw new InvalidUsageException("endianness attribute argument not found");
+
+ bool? structIsLittleEndian = isLittleEndianArgumentSyntax.Expression.Kind() switch
+ {
+ SyntaxKind.FalseLiteralExpression => false,
+ SyntaxKind.TrueLiteralExpression => true,
+ SyntaxKind.DefaultLiteralExpression => false,
+ _ => throw new InvalidUsageException($"{IsLittleEndianProperty} has to be set with a literal")
+ };
+ return structIsLittleEndian.Value;
+ }
+}
diff --git a/EndiannessSourceGenerator/EndiannessSourceGenerator.csproj b/EndiannessSourceGenerator/EndiannessSourceGenerator.csproj
new file mode 100644
index 0000000..99670d2
--- /dev/null
+++ b/EndiannessSourceGenerator/EndiannessSourceGenerator.csproj
@@ -0,0 +1,23 @@
+
+
+
+ netstandard2.0
+ false
+ enable
+ latest
+
+ true
+ true
+ EndiannessSourceGenerator
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
diff --git a/EndiannessSourceGenerator/Readme.md b/EndiannessSourceGenerator/Readme.md
new file mode 100644
index 0000000..a69efd1
--- /dev/null
+++ b/EndiannessSourceGenerator/Readme.md
@@ -0,0 +1,5 @@
+# Endianness Source Generator
+
+When annotating a struct with the `StructEndianness` attribute, this code generator will generate properties for the declared fields.
+
+Each time a property is read or written, the endianness is converted from runtime endianness to struct endianness or vice-versa.
diff --git a/TanksServer.sln b/TanksServer.sln
index 0580f66..644ced9 100644
--- a/TanksServer.sln
+++ b/TanksServer.sln
@@ -4,6 +4,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TanksServer", "TanksServer\
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DisplayCommands", "DisplayCommands\DisplayCommands.csproj", "{B4B43561-7A2C-486B-99F7-E58A67BC370A}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EndiannessSourceGenerator", "EndiannessSourceGenerator\EndiannessSourceGenerator.csproj", "{D77FE880-F2B8-43B6-8B33-B6FA089CC337}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -18,5 +20,9 @@ Global
{B4B43561-7A2C-486B-99F7-E58A67BC370A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B4B43561-7A2C-486B-99F7-E58A67BC370A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B4B43561-7A2C-486B-99F7-E58A67BC370A}.Release|Any CPU.Build.0 = Release|Any CPU
+ {D77FE880-F2B8-43B6-8B33-B6FA089CC337}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {D77FE880-F2B8-43B6-8B33-B6FA089CC337}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {D77FE880-F2B8-43B6-8B33-B6FA089CC337}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {D77FE880-F2B8-43B6-8B33-B6FA089CC337}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal