For the server we are hoping to have a code gen to create the packet reader and writers for the messages and maybe to also be used on the file formats. The idea is you define the class and annotate the fields and then have the code gen generate the read and write functions, the benfit of this is avoid having to write uguly error prone code over and over again and allows us to make sweeping changes to the readers and writers with out having to re write all those functions. Planning to use .Net 5 Source Generators. https://devblogs.microsoft.com/dotnet/introducing-c-source-generators/
An other plus from this should be better packet peep inspection, the code gen can also create imgui views for each class with better views on the data and better tracking of reads.
Why Aero, well she did talk alot so....
Install the nuget package Aero.Gen
The following functions will be generated on Aero marked classes
int Unpack(ReadOnlySpan<byte> data)
Unpacks the given span into the class, the return value is how many bytes were read from the data span.
If the return int is negative then the unpack failed and that how many bytes were read. (Needs to be compiled with bounds check on)
var TestPacket = new TestPacket();
int readBytes = TestPacket.Unpack(packetData);
int GetPackedSize()
Calculate the size in bytes this class will have when packed.
This function will preform much the same logic used for packing but just record the size.
var TestPacket = new TestPacket();
int SizeThatWillBePacked = TestPacket.GetPackedSize();
int Pack(Span<byte> buffer)
Packs this class into the buffer and returns how many bytes where packed.
The given buffer span should be atleast big enough to pack all the fields into.
Span<byte> buffer = new byte[10000];
var TestPacket = new TestPacket();
int packedSize = TestPacket.Pack(buffer);
- byte
- char
- int
- uint
- long
- ulong
- short
- ushort
- float
- double
- string
- Vector2
- Vector3
- Vector4
- Quaternion
Marks a class as one that should have readers, writers and such generated for it.
Marks a struct as one that can be serialised and included in an Aero class.
If a struct isn't marked with this it won't be serialised.
Marks a field as an array, there are a few variants of this.
AeroArray(int length)
: eg[AeroArray(2)]
Will read 2 values of the type of the array this is attached toAeroArray(string nameOfFeild)
: like the normal use only will take the length from the named field.- The named field must be a number type, eg, byte, short, int
- Should use
nameof
eg[AeroArray(nameof(ArrayLen))]
AeroArray(Type lengthType)
read a number type of that type and use that for the length of the array- eg.
AeroArray(typeof(uint))
Will read a uint and then read that value number of elements.
- eg.
AeroArray(int -length)
: eg[AeroArray(-4)]
If the fixed size is negative then the array will be crated with that number positive but will keep reading untill the end of the data is reached- eg.
[AeroArray(-4)] public int Test;
will create an array of ints with a size of 4 and do awhile(!hasReachedTheEnd)
for reading - The size given as the arg should be the max size that the array can have, it won't be resized
- There will be a
Get[ArrayName]Count
getter added that has the number of items that were read for this array.
- eg.
A field with this will be conditionally serialised if the logic passes.
[AeroIf(nameof(TestValue), 1)]
: Equivalent toif (TestValue == 1)
around the read[AeroIf(nameof(TestValue), 1, 2)]
: Equivalent toif (TestValue == 1 || TestValue == 2)
around the read[AeroIf(nameof(TestValue), Op.NotEqual, 1)]
: Equivalent toif (TestValue != 1)
around the read[AeroIf(nameof(TestValue), Op.HasFlag, Flags.Flag1)]
: Equivalent toif (TestValue & Flags.Flag1)
around the read
Multiple values in the one attribute will be ored as you can see above, to get and logic do like below:
[AeroIf(nameof(TestValue), 1)]
[AeroIf(nameof(TestValue), 2)]
public int TestInt;
and creates code equivalent to:
if ((Byte == 0) && (Byte == 1))
{
}
The ops options are:
- Equal
- NotEqual
- HasFlag
- DoesntHaveFlag
Strings should be marked with this attribute to be parsed correctly.
[AeroString(nameof(LenghtField))]
: Similar to the array attribute you can pass the name of a already defined numeric value to be used as the length of the string[AeroString(typeof(byte))]
: Marks the string as length prefixed with the given type, eg will read a byte and then that number of chars into the string[AeroString(10)]
: Read a fixed size string, in this case will read 10 chars as a string[AeroString]
: Defines a null terminated string, will read until it gets a0x00
or it reaches the end of the span.
[AeroSdb("Table Name", "Column Name")]
: Mark a field as having a value from the sdb table and column given.
Here are some exampls, for more you can see the unit tests in the project.
A basic example for all that is needed to have basic value types serialised
[Aero]
public partial class SimpleTypes
{
public byte Byte;
public char Char;
public int IntTest;
public uint UintTest;
public short ShortTest;
public ushort UshortTest;
public long Long;
public ulong ULong;
public float Float;
public double Double;
}
A basic example with arrays and some logic
[Aero]
public partial class Example2
{
public byte Byte;
public char Char;
// Read 4 ints
[AeroArray(4)]
public int[] FourInts;
// Read an int and uses it to get the length of the array and then reads them
[AeroArray(typeof(int))]
public int[] VarableIntArray;
// If the Byte value is 1 then the value is read, other wise it isn't
[AeroIf(nameof(Byte), 1)]
public int ShouldBeReadIfByteIs1;
}
A basic example of string parsing
[Aero]
public partial class Example3
{
[AeroString]
public string NulNullTerminatedString;
[AeroString(10)]
public string FixedSizeString;
[AeroString(typeof(byte))]
public string ByteLengthPrefixedString;
public int StringLength;
[AeroString(nameof(StringLength))]
public string VarablePrefixedLengthString;
}
To mark a class as a view add the attribute [Aero(AeroGenTypes.View)]
or [Aero(AeroGenTypes.Controller)]
to it, the true marks it as a view and will genrate the extra data.
Fields in the class will get a ShadowField
idx based on the order that they are defined in.
Nullables can be marked as such with the [AeroNullable]
Example:
[Aero(AeroGenTypes.View)]
public partial class SimpleViewWithNullable
{
public int TestValue;
public float TestVlaue2;
[AeroNullable] public int TestValueNullable;
}
The added functions are:
GetPackedSize()
UnpackChanges(ReadOnlySpan<byte> data)
PackChanges(Span<byte> buffer, bool clearDirtyAfterSend = true)
ShadowFieldIdToName(int id)
Returns astring
name for the shadow field with this idShadowFieldIdToType(int id)
Returns theType
for the shadow field with this idGetShadowFieldsData()
Returns an array of(string, int, Type, bool)
for all the shadowfields in this class, (name, id, Type, nullable)
If compiled with the Diag Logging enabled then there will also be a function GetDiagReadLogs()
that will return a List<AeroReadLog>
for each read that was done in the Unpack
or UnpackChanges
functions.
For each field in a view call a property will be genrated, for non nullables the set on this will mark that value as dirty so a PackChanges
call will only pack what has changed.
This is why feilds should be defined as private to ensure only the propetys can be called and those changes can be tracked.
These views shouldn't be shared or polled for this reason.
The following settings can be used in a .editorconfig file to adjust the generators output (or it should).
Aero_Enabled
: Enable or disable the generatorAero_BoundsCheck
: Enable or disable bounds checking for the unpacker, will return -bytes read if it couldn't read more from the passed bufferAero_DiagLogging
: Enable or disable diagnostic logging from the packer / unpackersAero_LogReadsWrites
: Enable or disable logging the reading or writing done by the packers or unpackers (just the unpackers atm), needs Aero_DiagLogging enabled too.