From a63c8ebbc40b76161553d0150e2a302a13a814a5 Mon Sep 17 00:00:00 2001 From: Sewer 56 Date: Tue, 5 Jul 2022 15:21:05 +0100 Subject: [PATCH 01/16] Target LNL v1.X-rc --- Source/Reloaded.Messaging/Reloaded.Messaging.csproj | 2 +- Source/Reloaded.Messaging/SimpleHost.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Source/Reloaded.Messaging/Reloaded.Messaging.csproj b/Source/Reloaded.Messaging/Reloaded.Messaging.csproj index 885375b..9b0094f 100644 --- a/Source/Reloaded.Messaging/Reloaded.Messaging.csproj +++ b/Source/Reloaded.Messaging/Reloaded.Messaging.csproj @@ -30,7 +30,7 @@ - + diff --git a/Source/Reloaded.Messaging/SimpleHost.cs b/Source/Reloaded.Messaging/SimpleHost.cs index 7ef8790..b846793 100644 --- a/Source/Reloaded.Messaging/SimpleHost.cs +++ b/Source/Reloaded.Messaging/SimpleHost.cs @@ -71,7 +71,7 @@ private void ListenerOnConnectionRequestEvent(ConnectionRequest request) } /* On each message received. */ - private void OnNetworkReceive(NetPeer peer, NetPacketReader reader, DeliveryMethod deliverymethod) + private void OnNetworkReceive(NetPeer peer, NetPacketReader reader, byte channel, DeliveryMethod deliverymethod) { byte[] rawBytes = reader.GetRemainingBytes(); var rawNetMessage = new RawNetMessage(rawBytes, peer, reader, deliverymethod); From a129ea574ca713d32ffffed44a809cf9a78ec682 Mon Sep 17 00:00:00 2001 From: Sewer 56 Date: Tue, 5 Jul 2022 21:45:56 +0100 Subject: [PATCH 02/16] Added: Missing documentation from functions. --- .../ReloadedMemorySerializer.cs | 3 +++ Source/Reloaded.Messaging/MessageHandler.cs | 10 +++++--- .../Messages/IMessageExtensions.cs | 8 ++++++ Source/Reloaded.Messaging/Messages/Message.cs | 16 ++++++++++-- .../Messages/MessageBase.cs | 9 +++++++ Source/Reloaded.Messaging/Overrides.cs | 7 ++++++ Source/Reloaded.Messaging/SimpleHost.cs | 8 ++++-- .../Reloaded.Messaging/Structs/NetMessage.cs | 23 +++++++++++++++++ .../Structs/RawNetMessage.cs | 25 +++++++++++++++++++ 9 files changed, 101 insertions(+), 8 deletions(-) diff --git a/Source/Reloaded.Messaging.Serializer.ReloadedMemory/ReloadedMemorySerializer.cs b/Source/Reloaded.Messaging.Serializer.ReloadedMemory/ReloadedMemorySerializer.cs index 414b0a7..83711a9 100644 --- a/Source/Reloaded.Messaging.Serializer.ReloadedMemory/ReloadedMemorySerializer.cs +++ b/Source/Reloaded.Messaging.Serializer.ReloadedMemory/ReloadedMemorySerializer.cs @@ -3,6 +3,9 @@ namespace Reloaded.Messaging.Serializer.ReloadedMemory { + /// + /// Serializes messages using raw byte conversion with Reloaded.Memory. + /// public class ReloadedMemorySerializer : ISerializer { /// diff --git a/Source/Reloaded.Messaging/MessageHandler.cs b/Source/Reloaded.Messaging/MessageHandler.cs index 2f342d9..080237c 100644 --- a/Source/Reloaded.Messaging/MessageHandler.cs +++ b/Source/Reloaded.Messaging/MessageHandler.cs @@ -7,13 +7,14 @@ namespace Reloaded.Messaging { /// /// Provides a generic mechanism for dispatching messages received from a client or server. - /// Works by assigning functions to specified message "types", declared by . + /// Works by assigning functions to specified message "types", declared by TMessageType. /// /// Type of value to map to individual message handlers. public class MessageHandler where TMessageType : unmanaged { private Dictionary _mapping; + /// public MessageHandler() { _mapping = new Dictionary(); @@ -32,7 +33,7 @@ public void Handle(ref RawNetMessage parameters) } /// - /// Sets a method to execute handling a specific + /// Sets a method to execute handling a specific /// public void AddOrOverrideHandler(Handler handler) where TStruct : IMessage, new() { @@ -41,7 +42,7 @@ public void Handle(ref RawNetMessage parameters) } /// - /// Sets a method to execute handling a specific + /// Sets a method to execute handling a specific /// public void AddOrOverrideHandler(TMessageType messageType, Handler handler) where TStruct : IMessage, new() { @@ -56,13 +57,14 @@ public void Handle(ref RawNetMessage parameters) } /// - /// Removes the current method assigned to a handle a message of a specific + /// Removes the current method assigned to a handle a message of a specific /// public void RemoveHandler(TMessageType messageType) { _mapping.Remove(messageType); } + /// public delegate void Handler(ref NetMessage netMessage); private delegate void RawNetMessageHandler(ref RawNetMessage rawNetMessage); } diff --git a/Source/Reloaded.Messaging/Messages/IMessageExtensions.cs b/Source/Reloaded.Messaging/Messages/IMessageExtensions.cs index 6ae51ca..cff5ed9 100644 --- a/Source/Reloaded.Messaging/Messages/IMessageExtensions.cs +++ b/Source/Reloaded.Messaging/Messages/IMessageExtensions.cs @@ -2,8 +2,16 @@ namespace Reloaded.Messaging.Messages { + /// + /// Hosts various extension methods related to the interface. + /// public static class MessageExtensions { + /// + /// Retrieves the message type (key) for a given type. + /// + /// The message to get type for. + /// Used to instantiate and get message type (key) as single liner in source code. public static TMessageType GetMessageType(this IMessage message) where TMessageType : unmanaged { return message.GetMessageType(); diff --git a/Source/Reloaded.Messaging/Messages/Message.cs b/Source/Reloaded.Messaging/Messages/Message.cs index b8d3a68..308a28e 100644 --- a/Source/Reloaded.Messaging/Messages/Message.cs +++ b/Source/Reloaded.Messaging/Messages/Message.cs @@ -5,16 +5,29 @@ namespace Reloaded.Messaging.Messages { + /// + /// + /// + /// The key for the message. + /// public unsafe class Message : MessageBase where TStruct : IMessage, new() where TMessageType : unmanaged { // ReSharper disable StaticMemberInGenericType private static ISerializer DefaultSerializer { get; set; } private static ICompressor DefaultCompressor { get; set; } private static bool DefaultCompressorSet { get; set; } + // ReSharper restore StaticMemberInGenericType + /// + /// The actual message backed by this class. + /// public TStruct ActualMessage { get; set; } + /// + /// + /// + /// public Message(TStruct message) { ActualMessage = message; @@ -69,10 +82,9 @@ public static TStruct Deserialize(byte[] serializedBytes) // Get serializer var serializer = GetSerializer(); - // Read messagepack message. + // Read message. var messageSegment = serializedBytes.AsSpan(sizeof(TMessageType)).ToArray(); var message = serializer.Deserialize(messageSegment); - return message; // Note: No need to read MessageType. MessageType was only necessary to link a message to correct handler. diff --git a/Source/Reloaded.Messaging/Messages/MessageBase.cs b/Source/Reloaded.Messaging/Messages/MessageBase.cs index 72f00e2..9889986 100644 --- a/Source/Reloaded.Messaging/Messages/MessageBase.cs +++ b/Source/Reloaded.Messaging/Messages/MessageBase.cs @@ -1,7 +1,16 @@ namespace Reloaded.Messaging.Messages { + /// + /// The base class for the class, used for dealing with message type specific things. + /// + /// public unsafe class MessageBase where TMessageType : unmanaged { + /// + /// Retrieves the message type by raw array of values. + /// + /// The raw serialized bytes containing the message. + /// The type of the message. public static TMessageType GetMessageType(byte[] serializedBytes) { fixed (byte* arrayPtr = serializedBytes) diff --git a/Source/Reloaded.Messaging/Overrides.cs b/Source/Reloaded.Messaging/Overrides.cs index 4953eea..8bc95dc 100644 --- a/Source/Reloaded.Messaging/Overrides.cs +++ b/Source/Reloaded.Messaging/Overrides.cs @@ -10,7 +10,14 @@ namespace Reloaded.Messaging /// public static class Overrides { + /// + /// Allows you to override the serializer for a specific type. + /// public static Dictionary SerializerOverride { get; } = new Dictionary(); + + /// + /// Allows you to override the compressor for a specific type. + /// public static Dictionary CompressorOverride { get; } = new Dictionary(); } } diff --git a/Source/Reloaded.Messaging/SimpleHost.cs b/Source/Reloaded.Messaging/SimpleHost.cs index b846793..2f1e599 100644 --- a/Source/Reloaded.Messaging/SimpleHost.cs +++ b/Source/Reloaded.Messaging/SimpleHost.cs @@ -25,7 +25,7 @@ public class SimpleHost : IDisposable where TMessageType : unmanag public event EventBasedNetListener.OnConnectionRequest ConnectionRequestEvent; /// - /// Dispatcher for individual (s) to your events. + /// Dispatcher for individual (s) to your events. /// public MessageHandler MessageHandler { get; private set; } @@ -35,7 +35,11 @@ public class SimpleHost : IDisposable where TMessageType : unmanag /// public NetManager NetManager { get; private set; } - + /// + /// Provides a simple client or host based off of LiteNetLib. + /// + /// Set to true to accept incoming clients, else reject all requests. + /// The password necessary to join. public SimpleHost(bool acceptClients, string password = "") { Password = password; diff --git a/Source/Reloaded.Messaging/Structs/NetMessage.cs b/Source/Reloaded.Messaging/Structs/NetMessage.cs index b08fa71..b4c8a30 100644 --- a/Source/Reloaded.Messaging/Structs/NetMessage.cs +++ b/Source/Reloaded.Messaging/Structs/NetMessage.cs @@ -2,13 +2,36 @@ namespace Reloaded.Messaging.Structs { + /// + /// Encapsulates a message with a specific structure received from the network as a raw array of bytes. + /// public struct NetMessage { + /// + /// The message received from the peer. + /// public TStruct Message { get; private set; } + + /// + /// The peer which has sent you the message. + /// public NetPeer Peer { get; private set; } + + /// + /// Can be used to read the message, if desired. + /// public NetPacketReader PacketReader { get; private set; } + + /// + /// The method via which the package was delivered. + /// public DeliveryMethod DeliveryMethod { get; private set; } + /// + /// Encapsulates a raw message received from the network. + /// + /// The message in question. + /// The raw message from which this message should be constructed with. public NetMessage(ref TStruct message, ref RawNetMessage rawMessage) { Message = message; diff --git a/Source/Reloaded.Messaging/Structs/RawNetMessage.cs b/Source/Reloaded.Messaging/Structs/RawNetMessage.cs index 0f6bc49..87e7f65 100644 --- a/Source/Reloaded.Messaging/Structs/RawNetMessage.cs +++ b/Source/Reloaded.Messaging/Structs/RawNetMessage.cs @@ -2,13 +2,38 @@ namespace Reloaded.Messaging.Structs { + /// + /// Encapsulates a message received from the network as a raw array of bytes. + /// public struct RawNetMessage { + /// + /// The contents of the message. + /// public byte[] Message { get; private set; } + + /// + /// The peer from whom the message was received from. + /// public NetPeer Peer { get; private set; } + + /// + /// Used to read the packet internals, if desired. + /// public NetPacketReader PacketReader { get; private set; } + + /// + /// The method via which this message was delivered. + /// public DeliveryMethod DeliveryMethod { get; private set; } + /// + /// Encapsulates the message received from the network as a raw array of bytes. + /// + /// The message in question. + /// The peer from whom the message was received from. + /// Used to read packet internals, if desired. + /// The method via which the message was delivered. public RawNetMessage(byte[] message, NetPeer peer, NetPacketReader packetReader, DeliveryMethod deliveryMethod) { Message = message; From 0f90621a82b6aa2c9dce04f27cee302138708e86 Mon Sep 17 00:00:00 2001 From: Sewer 56 Date: Tue, 5 Jul 2022 22:15:44 +0100 Subject: [PATCH 03/16] Added: Suppression for unwanted NuGet Warning --- Source/Reloaded.Messaging/Reloaded.Messaging.csproj | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/Source/Reloaded.Messaging/Reloaded.Messaging.csproj b/Source/Reloaded.Messaging/Reloaded.Messaging.csproj index 9b0094f..d67c211 100644 --- a/Source/Reloaded.Messaging/Reloaded.Messaging.csproj +++ b/Source/Reloaded.Messaging/Reloaded.Messaging.csproj @@ -17,16 +17,8 @@ true $(DefineConstants);USE_NATIVE_SPAN_API $(DefineConstants);USE_NATIVE_UNSAFE - - - - true - obj\\Reloaded.Messaging.xml - - - + 1701;1702;NU5104 true - obj\\Reloaded.Messaging.xml From 53cd4eb0952c434b8a86755212f797f737f06749 Mon Sep 17 00:00:00 2001 From: Sewer 56 Date: Tue, 5 Jul 2022 23:36:46 +0100 Subject: [PATCH 04/16] Update: All NuGet Packages --- ...aded.Messaging.Compressor.ZStandard.csproj | 2 +- .../MsgPackSerializer.cs | 24 ++++++++----------- ...ed.Messaging.Serializer.MessagePack.csproj | 2 +- .../NewtonsoftJsonSerializer.cs | 3 +++ ...Messaging.Serializer.ReloadedMemory.csproj | 2 +- ...Messaging.Serializer.SystemTextJson.csproj | 2 +- .../Reloaded.Messaging.Tests.csproj | 11 +++++---- .../Struct/StringMessage.cs | 2 +- .../Struct/Vector3.cs | 2 +- .../Tests/Serialization/StringPassTest.cs | 2 +- .../Tests/Serialization/VectorPassTest.cs | 2 +- .../Reloaded.Messaging.csproj | 6 ++--- 12 files changed, 31 insertions(+), 29 deletions(-) diff --git a/Source/Reloaded.Messaging.Compressor.ZStandard/Reloaded.Messaging.Compressor.ZStandard.csproj b/Source/Reloaded.Messaging.Compressor.ZStandard/Reloaded.Messaging.Compressor.ZStandard.csproj index bccd177..50b4a98 100644 --- a/Source/Reloaded.Messaging.Compressor.ZStandard/Reloaded.Messaging.Compressor.ZStandard.csproj +++ b/Source/Reloaded.Messaging.Compressor.ZStandard/Reloaded.Messaging.Compressor.ZStandard.csproj @@ -24,7 +24,7 @@ - + diff --git a/Source/Reloaded.Messaging.Serializer.MessagePack/MsgPackSerializer.cs b/Source/Reloaded.Messaging.Serializer.MessagePack/MsgPackSerializer.cs index c6b39e2..88e651b 100644 --- a/Source/Reloaded.Messaging.Serializer.MessagePack/MsgPackSerializer.cs +++ b/Source/Reloaded.Messaging.Serializer.MessagePack/MsgPackSerializer.cs @@ -1,4 +1,5 @@ using MessagePack; +using MessagePack.Resolvers; using Reloaded.Messaging.Interfaces; namespace Reloaded.Messaging.Serializer.MessagePack @@ -6,45 +7,40 @@ namespace Reloaded.Messaging.Serializer.MessagePack public class MsgPackSerializer : ISerializer { /// - /// Uses LZ4 compression for serialization. + /// Any custom resolver to pass to MessagePack. + /// Default is /// - public bool UseLZ4 { get; private set; } + public IFormatterResolver Resolver { get; private set; } = ContractlessStandardResolver.Instance; /// - /// Any custom resolver to pass to MessagePack. - /// Default is + /// Options for the MessagePack serializer. /// - public IFormatterResolver Resolver { get; private set; } = global::MessagePack.Resolvers.ContractlessStandardResolver.Instance; + public MessagePackSerializerOptions SerializerOptions { get; private set; } = MessagePackSerializerOptions.Standard; /// /// Creates a new instance of the MessagePack serializer. /// - /// Uses MessagePack's serializer with LZ4 compression. /// /// Custom resolver to pass to MessagePack, default is "Contractless Resolver" - /// (). + /// (). /// - public MsgPackSerializer(bool useLz4, IFormatterResolver resolver = null) + public MsgPackSerializer(IFormatterResolver resolver = null) { - UseLZ4 = useLz4; if (resolver != null) Resolver = resolver; } - /// public TStruct Deserialize(byte[] serialized) { - return UseLZ4 ? LZ4MessagePackSerializer.Deserialize(serialized, Resolver) : - MessagePackSerializer.Deserialize(serialized, Resolver); + return MessagePackSerializer.Deserialize(serialized, SerializerOptions); } /// public byte[] Serialize(ref TStruct item) { - return UseLZ4 ? LZ4MessagePackSerializer.Serialize(item, Resolver) : - MessagePackSerializer.Serialize(item, Resolver); + return MessagePackSerializer.Serialize(item, SerializerOptions); } } } diff --git a/Source/Reloaded.Messaging.Serializer.MessagePack/Reloaded.Messaging.Serializer.MessagePack.csproj b/Source/Reloaded.Messaging.Serializer.MessagePack/Reloaded.Messaging.Serializer.MessagePack.csproj index b6eacd5..9ec24ad 100644 --- a/Source/Reloaded.Messaging.Serializer.MessagePack/Reloaded.Messaging.Serializer.MessagePack.csproj +++ b/Source/Reloaded.Messaging.Serializer.MessagePack/Reloaded.Messaging.Serializer.MessagePack.csproj @@ -26,7 +26,7 @@ - + diff --git a/Source/Reloaded.Messaging.Serializer.NewtonsoftJson/NewtonsoftJsonSerializer.cs b/Source/Reloaded.Messaging.Serializer.NewtonsoftJson/NewtonsoftJsonSerializer.cs index 86d9eaf..7cc317d 100644 --- a/Source/Reloaded.Messaging.Serializer.NewtonsoftJson/NewtonsoftJsonSerializer.cs +++ b/Source/Reloaded.Messaging.Serializer.NewtonsoftJson/NewtonsoftJsonSerializer.cs @@ -6,6 +6,9 @@ namespace Reloaded.Messaging.Serializer.NewtonsoftJson { + /// + /// + /// public class NewtonsoftJsonSerializer : ISerializer { /// diff --git a/Source/Reloaded.Messaging.Serializer.ReloadedMemory/Reloaded.Messaging.Serializer.ReloadedMemory.csproj b/Source/Reloaded.Messaging.Serializer.ReloadedMemory/Reloaded.Messaging.Serializer.ReloadedMemory.csproj index 9ebc6dc..0a1c0ef 100644 --- a/Source/Reloaded.Messaging.Serializer.ReloadedMemory/Reloaded.Messaging.Serializer.ReloadedMemory.csproj +++ b/Source/Reloaded.Messaging.Serializer.ReloadedMemory/Reloaded.Messaging.Serializer.ReloadedMemory.csproj @@ -24,7 +24,7 @@ - + diff --git a/Source/Reloaded.Messaging.Serializer.SystemTextJson/Reloaded.Messaging.Serializer.SystemTextJson.csproj b/Source/Reloaded.Messaging.Serializer.SystemTextJson/Reloaded.Messaging.Serializer.SystemTextJson.csproj index 4dd3ae0..d365a5c 100644 --- a/Source/Reloaded.Messaging.Serializer.SystemTextJson/Reloaded.Messaging.Serializer.SystemTextJson.csproj +++ b/Source/Reloaded.Messaging.Serializer.SystemTextJson/Reloaded.Messaging.Serializer.SystemTextJson.csproj @@ -17,7 +17,7 @@ - + diff --git a/Source/Reloaded.Messaging.Tests/Reloaded.Messaging.Tests.csproj b/Source/Reloaded.Messaging.Tests/Reloaded.Messaging.Tests.csproj index 6641fe3..a51d27b 100644 --- a/Source/Reloaded.Messaging.Tests/Reloaded.Messaging.Tests.csproj +++ b/Source/Reloaded.Messaging.Tests/Reloaded.Messaging.Tests.csproj @@ -1,7 +1,7 @@  - netcoreapp3.0;NET472 + netcoreapp3.1;NET472;net5.0 false @@ -17,9 +17,12 @@ - - - + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/Source/Reloaded.Messaging.Tests/Struct/StringMessage.cs b/Source/Reloaded.Messaging.Tests/Struct/StringMessage.cs index 1fd28e7..820d475 100644 --- a/Source/Reloaded.Messaging.Tests/Struct/StringMessage.cs +++ b/Source/Reloaded.Messaging.Tests/Struct/StringMessage.cs @@ -8,7 +8,7 @@ namespace Reloaded.Messaging.Tests.Struct public struct StringMessage : IMessage { public MessageType GetMessageType() => MessageType.String; - public ISerializer GetSerializer() => new MsgPackSerializer(true); + public ISerializer GetSerializer() => new MsgPackSerializer(); public ICompressor GetCompressor() => null; public string Text { get; set; } diff --git a/Source/Reloaded.Messaging.Tests/Struct/Vector3.cs b/Source/Reloaded.Messaging.Tests/Struct/Vector3.cs index 08a2f8d..ad1d821 100644 --- a/Source/Reloaded.Messaging.Tests/Struct/Vector3.cs +++ b/Source/Reloaded.Messaging.Tests/Struct/Vector3.cs @@ -7,7 +7,7 @@ namespace Reloaded.Messaging.Tests.Struct public struct Vector3 : IMessage { public MessageType GetMessageType() => MessageType.Vector3; - public ISerializer GetSerializer() => new MsgPackSerializer(true); + public ISerializer GetSerializer() => new MsgPackSerializer(); public ICompressor GetCompressor() => null; public float X { get; set; } diff --git a/Source/Reloaded.Messaging.Tests/Tests/Serialization/StringPassTest.cs b/Source/Reloaded.Messaging.Tests/Tests/Serialization/StringPassTest.cs index c3e08e8..45c19cd 100644 --- a/Source/Reloaded.Messaging.Tests/Tests/Serialization/StringPassTest.cs +++ b/Source/Reloaded.Messaging.Tests/Tests/Serialization/StringPassTest.cs @@ -36,7 +36,7 @@ public void Dispose() [Fact(Timeout = 1000)] public void MsgPackPassString() { - Overrides.SerializerOverride[typeof(StringMessage)] = new MsgPackSerializer(false); + Overrides.SerializerOverride[typeof(StringMessage)] = new MsgPackSerializer(); Overrides.CompressorOverride[typeof(StringMessage)] = null; PassString(); } diff --git a/Source/Reloaded.Messaging.Tests/Tests/Serialization/VectorPassTest.cs b/Source/Reloaded.Messaging.Tests/Tests/Serialization/VectorPassTest.cs index 6b240bc..384eba9 100644 --- a/Source/Reloaded.Messaging.Tests/Tests/Serialization/VectorPassTest.cs +++ b/Source/Reloaded.Messaging.Tests/Tests/Serialization/VectorPassTest.cs @@ -34,7 +34,7 @@ public void Dispose() [Fact(Timeout = 1000)] public void MsgPackPassVector3() { - Overrides.SerializerOverride[typeof(Vector3)] = new MsgPackSerializer(false); + Overrides.SerializerOverride[typeof(Vector3)] = new MsgPackSerializer(); Overrides.CompressorOverride.Remove(typeof(Vector3)); PassVector3(); } diff --git a/Source/Reloaded.Messaging/Reloaded.Messaging.csproj b/Source/Reloaded.Messaging/Reloaded.Messaging.csproj index d67c211..9d8bc13 100644 --- a/Source/Reloaded.Messaging/Reloaded.Messaging.csproj +++ b/Source/Reloaded.Messaging/Reloaded.Messaging.csproj @@ -1,7 +1,7 @@  - netstandard2.1;netstandard2.0;netcoreapp3.1 + netstandard2.1;netstandard2.0;netcoreapp3.1;net5.0 false Sewer56 @@ -23,8 +23,8 @@ - - + + From 33d8be193bbe84a41a8ee5475872f38ce5a5445f Mon Sep 17 00:00:00 2001 From: Sewer 56 Date: Tue, 5 Jul 2022 23:46:07 +0100 Subject: [PATCH 05/16] Added: NuGet Icons into every .nupkg --- Source/NuGet-Icon.png | Bin 0 -> 47501 bytes .../Reloaded.Messaging.Interfaces.csproj | 6 +++++- ...aded.Messaging.Serializer.MessagePack.csproj | 7 ++++++- ...d.Messaging.Serializer.NewtonsoftJson.csproj | 6 +++++- ...d.Messaging.Serializer.ReloadedMemory.csproj | 7 ++++++- ...d.Messaging.Serializer.SystemTextJson.csproj | 6 +++++- .../Reloaded.Messaging.csproj | 7 ++++++- 7 files changed, 33 insertions(+), 6 deletions(-) create mode 100644 Source/NuGet-Icon.png diff --git a/Source/NuGet-Icon.png b/Source/NuGet-Icon.png new file mode 100644 index 0000000000000000000000000000000000000000..13e84a31ce734a4e3b8a8b7de49c4a3dafe03f19 GIT binary patch literal 47501 zcmV)RK(oJzP)5+p zhI;7n;xF*4^LzrYKn8J(+y7*X{_S|4_73z3@fz4J=(X~_Mf&SufnozR!wkiG=!7mU zu~rW>>xvc70ZAvU)e_&pS1vK0xW(;%2Kv|G<>$4*OOS#_SZ%&vFY!x}c=h~1e2ck% zozWrvYhX2jexo0%VHqrUa7yzmbbK}x!*B3Dj3aJwdv5M6Fy4d-(zej7{LXXHk4d`5 zYG;eNyaASAwIluWM1PHlPY=YmOaE$#@3w1&GxR|KUX!n0ah3t z8fKZ%1C`>wL?@JKo<-hRszvAPiV}P0=!68cIa@J&q#5ETL)`ui?q7v3pk7R`z)m;^ z&)^9h&;YqG&WO@nXoA(wM#-*~(RJz?n5hj`LLKxdR!DyS$ z$27o)Fh2eC|BZ}pK+0uwjg;5oIgh$dWF?+PFRK2O{blMd#S&-~(?v!r)W8D6Gau$UTeoD-lK38L{XWU=@p`AY>k3-b zed9yVWh~ULDe) z?zd{H4Ngc&e?$BPcp*~0IDTyM+YVddKGJW#zmD{)&msNR{YRz$fv~=n{@!t#Iew{T zo26c??(yOx5pPi}Fxl53@j{sr_c;zI7V%luGx>5npcArXeL(JW?d|h=o-_2KD*<2bL>KM0uuV z&m#Hj)&>g#{qr4GtpR2mb&!M^Mw8U`iEOK6_dvhIXUTf6I@_tbq1`d{>JI6z_Lgp> z-|>Bc{(cvIg}9AT^gYnzJ0Z9S{}A-u@ZjI3KjeOiKNfQTQLFY()2xy+q;Qca&otjJ z)CpaRd9v7Kf}gFf2}OA=l!qMO011)pcW|B@_vnO7`R+y?&UV@$2O8AvkPp@BPUlI5 z+spAMWc63XZH%Hn9=?aoumS!g{eOXN@F>u~3vLGb_nP}d-5)9+ov=q_(@p;qgu7Ix zmzd_u1aY3%%PsL|34H}r!GKZeFv}Hcto6I3w%#1y;n!xEBI}*ds%{eZ7Avk=bt~jJ zTZdl@B)-ciafu$6$Qx@v2*mV@{y6#DEZOTt{O^8l73;t1hn+B__`T1Bvv}qs{f8ZP z9}YU~224tm{x`)d;a+o*(3c3}9HB3>$eAJWNu%HKDQ}T@svuWHk8|rJN0B`oF2Sz+)c*e?+Vm;lz`t z;ICbQesljW_YC`^D7`}xAuE#a`9fbJi?f8jG#r5Tg;J#3NP0`BQ5kl$9WtF6h_93K zM!z<~1g|s&S5&DR9iN0|Bi9vMy**)QDv}mI5pf$sWzT!i2oKHof6xZMM<=Ks`49;7 z^9Oqa{X90`AF=GYH;n$lC`zUY%4d*cQlBgI#qwvq_Xsi+-=!L$E8tiF?MBkuo29%` znj0j$TFPtvS}XlEK~I7d)a!?7=KBV}=D1>$vlVKvHkT;Z81E3bv4Z}aPzS5U`mX}L z&FpTzXA{zI?{=i$f@imN{}buoXWf6#IDp#&M=pz^x6=Hl`o(%?w$K+1`zb0R#N9$) zET$bsiF8UlWm4OqlgqKD7v+jj;neAh34YdkWg;ZiH8925>inAJiuKNxPgueM1a&v&N9`i$u0d;uD4iI>jdAMxz8W1M#I;)%dks$}6DS&)=Zju9UhY=t-_p?bm6p zUTfrrgV9D@PCXWW`HB<&x3Ed3cgysjrFn-Q@N9>zV*N+;cDN4iblvqj?4k4@68%6w zgVMh{ip-Zp>AdmZg!x|n8brU#>{}uF?J}A$88<0%px!8!+G?Xj`YS}aRQk)q0nxAa zrXZ2-E}i4LlSX^>AJhLfw20|#0{zmwQxE*fE8F3Yi9gVf zwZ8O+Q_lT?{@c>OL;45RT~V|WzE1P#)AJni1f3zq5p9J(7+?3|W z=J+0DV8LMDHItPWXnb*6eyvC%P z6wROEH{&zoOZeOk=!fu;yO)e%r?*oizRY_IvBFUotBduL&jE_m$>H?toNPr>gJn7< z1$pWksB?VWMl1R+36^y{LsI@weaHI#uKIz*59yG{uo*5Z9*X!$xbMiFf__UE?e&)H z#y(gWMXO;YRE~J37+HjLm~pEub*ix_7rQFC%vDq=rVhh5A~g6IO6^J@{pwQGqHMko zm5;h0=*h0)v`jI|)j4)mLNyeQ-n0Msfd3;mmyaZSP~z{YZ>evo@2LmP_Yd6zT=JF& zy5&UJ>FyGAce?D6{;S45{G+WXS^)#_2_tQU0u5fzHL8cvpOn-*2_AkN z`61@}Q1@4byk6$*73vaARjE#Ry%OvG;Dl)spY-1Bpi|QSDRCQx^uGp~uw0a%hTMN& zeM@~yeJ|AdkL}$C7r<&q`bGZ^((fdLA@^S~_L}wgM$vNVPrwKdKqKp%EFo)!Cb26H z$B{WkrM)>O=A@A;RW*Bi=`@p3#_j_Wb6``Lh$ zJ4vI^XF5-V+VlQkHoM-#W*aB;pSzo%NZdvh{o`PwiGRH)KXfm<4(_OjygmpI^uR_r zzONHD!x_x?>Z79XbAz2)=$iY14RFz?#4cD0r(kmw^~%#6X^R+9dr!lNqX+PzEXg#I zMwVn-`H(ODm1140p6puh!bCZ4v}>B5&6*+8@hx^`g*P3VB)-^-4NjZq3BkYlkL@Nq zZlmZIE~z>qrgz}J)UK8F`*OeDEB7UPqbT1K^ev)%$B|p$Fx&`xhMnGWRr=S%d1I&a zpY)xd9_gP6vtYuA&;Ijdv{q#E&EF}t^iv*ke}xbis;k2>(PZ_H__K!dwE8(sJGF&^ zs(RSgBBgF6yyxTf0%tBA8++WC{&%JDWT5|^)UJYC>Oq}wU429PFE~uiDSU@V`VWR$ zf2+`6wbtJ*{pXDBumq0#l$aSs37Fy1o#yqCv@gulDY4FjNJEu7AO7Q-w|qg|#sK~DeHJ!|@;#|tE#G%c;cNW7 zqX*VW|84aKYyBbhCh5N}^joCgTA!`bf7Tjlo4Nm(u|$RjV7f?Uk8HmW7wZU)2?Rym&=dY;lqcx?&Za_O30kz+0@c zD26`hhc?k{8d=5s*6^cJbNRPk`RrenA}_lQtrzY}JhRt7p+GnNhxh(_B|8_I)ZLah zb>X$5gfsN2ON=f@zDe9hfBJu4Mpwh2lwXwiYk}-VCUS{qInp1hqa#SX^zTw!lI%^P z*59aMPFY!PlKw-+Ke2Zf%8evR7{1bsTjN<~W@e^gW(G5e!9**WahTc6zP%SXqNh8@ zm+>p)(|W^mXlX~0p-8t&4b@*}W$~iEi@I+)4)`-bJh-xZF1pWWd{b>ZA1?LQYqMrK zR%ou)F%2u)U@Mwv?YBKz-(kC2t4?s{2cJT4|L@TiH3ivsm(m{v>uJ7|izplMq`zdu zV>yHJ_df#n9VAV%>xBEAoa+3dpr4@bYwON_W>Zf9>p%bD0bk7N{k6|Wpj-vq-@$Zd zHLIgGc~YGK)g1qZ;OZ^<&b&fE;DG@UbsPlW8GLp#-_xS}7Q~O?$S-3OV^DsEpN!z+ zyHvaF#29WN-HV1qv~!gN5dx&Mm`62I0{vfc^<%i(H~v!(hIcQ{^7lo{1Xr7kcugM* z0KGN*2M`-yWXFHd@{A8Y1?B!<2GdD=b_3s&_;UO`#Dnrhlo2eUA0xR-ouQqd+(Kzs z8tq&{Iss?^14!G_RL-3SGHaYUf{Xo_g6Kc;c@c=MDf5?Ws9$n#n}~wK%qH8({e%%4 z&uscutmwg~01eN+U;x1|W)SlBy+s-YZPecDzqMZ(ixv(f$ zMJ+!WpxmgyW-6UKNbtQ!-A|uD*uT40lp_9PDqg-N$XV6~F#b){ePdYcE`A$FY<^=5 z+4q3{!la)Tl}>ziBj4k|T~D+#s`|iLOu{UrXHfzA7J$Bv{7AA(*0uvl=N1L?sIi`4 zxUb+y+`0YMNDpDVZKxT4zJC8t@i1-qP6H6$CXy+GV_V&%-0Hr|MM~I70GyD)%eSZt$kD09vRX71(tLz`lv0ryYe z=OetYRjUqY{kBS;=m)MI(5kc*gfaVie&*JZ|qxXTn>1p*|)d9x^Y6Xo%J3~k_?zWd-wE{#3 z2GGPMnsNVe=f2DgxQG59tXUoXJ0kLxw`{%zpe2MT-{9%1xR$M29>N&5@SUJe^4&6P ztoyi`7Hy7n+;qvN*UO3N|^lo&=5kV^^$MJzoO=UP`-m-j}yAw z7V*92@6l7{@1egDXMYwHqB#40D6hfU_n?=JA@ywuG3}+<-D~zD)-0H8+q6$egD5D?hVieILk@=1#bW^0{s>1qM`}t zGTG6^!Hei^pnr}Gtpoi5bfdqAgO#GcK^yC)IDUj9O}vl8i{ji40tOqSwo?EcBUQ1tZ^onGT^)dx?3tZ}$a_@;UMI@=+$0u5(q)==MTyvha9 zU&C+b(5tBntt)H;_#W3DBG;}%e}wG5%<(8Wbq*tmLQ+3@8pq#sA+}#)JNa9OXKLYX z;b!nLYEZ9sZkMK+GpdY{%rJKVVT#GJvw>g5J?~TPmZ76+gn>dlpBBVLZ_uU~0TOsC z9%a1+zVEZY`9J^h$lf$rx|cav73`V7<#stUW6?0)L!@0pZXP6g1PP_z@~&+obUB;Q*&*u>R$ap^Xk z*~4^@tz^2rZ*>fISW9j<;VII*h#uTZ7wBKY5%j1(K8!x0vAP{N@;MTxwZD$M|y$&vFd|`??^T|KA2{qdx%C7VC$}sX_j0K}8Q)X}-2y9la%rcB1MtY6Q3g z+FCRM=rKHTiE|%nkHr02oad;f7j9Sc%cf;2UN30KT99NLwMQ@Xa|y1vflJ%ULOamY z+73R#BeYP$&NUYzht#G?Ue-(L}LbrDPCO|x5TS=qi@3r$B zN+bXM7|IVI)(3nX`HthZGsPC9_~&l;3vMzb`omTaf%urk`dC5|#G5&1Om}<093*lm z-^TN7(NpX|v5532lwU`x&_|GjrfW~D<}ix^&}2tBphgZNAJuv}KEW%Ph9H#yH3 zm$2(TT}biI3t)ZL=tmh;vq>NHhs|puA##COf^L6BJ0V_1Li8WZgd*rkb*65i@#hd? zi!7W#zF)d|)(#HlexBj#VKWjnJJu5I#AIL_`EG^&o!o$xREJ2@gRmPwUX5fXOl>7+ z+SLAbR6UYc$(D9FzGw6k0!@;j*(NselSXUh0;#z*ZpN24(B38H3ae{WH+7Nv(9@E84k@tC~U|BR|JUGsC;SWAt1;Q8y28eT?(tPdJ$oFT#SdUM8v__fX* z<=~e@v8Rw9(91dbcJyk_x$EaTgN|kB=hC(4F?BC&v)9iRcV_6v2os|Jv}(prtnL@I zPRn(bW(^8_g1qhIiCXc;=gGPhQOYeIEd`^aBxtdzd^~s~%Di>qoA2jWG0{JQUl=pr zuh!35&Je)NY$8;IvyISG^nL*tz(Y{=qv)r*6#CIaFq9?>$`w`D5V9vl|51x}R0l|a zW0K=_a>mDb0p-pe6aA+VqrU=SC4QCzB3R#;8v|(bq9}GH`e|%MkBj~tnBw>Rro1HsO46|1?-Hqs#$WAPwmTMiv`BiIR^Vix`>mbRZ7IdL! z=-w#1q{*z!=FqLPEKHhxc`|?ZQ)@uOwoCbQT~3S?uvtgy?zn>i82zKb-7Df} zmePL%eH{9SxeD|X?HK()9~Au)(VGPd)<&3ZpvYm4{4^k?qys4gEajlV1XP~f}Db0_J_lnFKli=+X^A_LD8QkVf=jFzq7!I4GYZ zmnQM+*Rtrpi$07#Mw(mbQ{?THEcypg#?7^9xKXPXHTyFEn*WhkZBYguqZ|5bVKD{1 zHNe`4RLkEdjDF1oh>89-js0xqJSl$Lq(RXKoLi*44B5oHtTy`5LuB_M(VU>%*q%lI z4ppoJ8YuArzWfQkUuUq|li2gx0+@zEtt{x=F6U~UYjm!joC%Y$-Gq*7mfFC3+Qnl# z35KCR3e%I&AJn+cYnhs^22I>E`q6KoPorN_9RT0Y@_y#~p?^fh?_DIxQ0jb(e>8wB zp;peo&(e>6OmjYh&|h!#E80nE+)i>tSC+5F-`lvr=|XCIosNNGXUBOv*-^k zPk)S*(63R=8sDE@N`FeNrO;1sF}&n^ugs@5*nAOAX5VaRTo=pY04 zQ}?|9f7YvYm1^rP=kNs-dKiP#N!}+Xhr#r$eE)Qo@1LWp5Y700a{HztpJax9^g)y< z+*!L8#YtJ9rJ^RJr9i&Sui+2Hz-RQ=ivBj}EEoM%IrN+FkBk0P4*ll)yO0|6dffl! zoVcF?N3~v+Ctu*~Ls;NPj1*eG%8^~Uthi~e7?_@;7wlPvIgFFHS2UJ$5D)S&nUfJe zvfH__#q`b%k-dxh6!Zu2{j<;?f&O{Xe@m^ONku->IG`cX-vT5PNQ(DvT@9U_>W-H9 zKFvMnjr)JsMpl7oK=fBbUWWdoa?CZ5S||FCq8R-rv*P|TerQuJs?d80XE#83C1NY| z+eHsh3Wd~;^t#^SGq^XGr_irla$k+!&RV>?pL$NerXtO0H>!&;{Vd!Uo1j0+e}9hw zv^qEI^UYvC<+&>L8MQ`}f4HL~;z^5enh+;5bW`UevBrZwmc&NHGS`PqHD*;7@Gm*1DM9xdrEj zd6jpddU4=QcdAf(Lavr%qk+IhEv1bg|jN{cnDU(Mm-zbz{_ ziR==f2m@G$mzhS)_x}z9I7fjg`bmlW0KOUedr}wN_$?*!JdFO49o#G&GWvVjz5H+dtKYd9+i+Nti??2`DZ$(P{{!+hxx8J|T??2%8 zzvuU__ph!`vETpDx^)lx{r_1hE-ETo-Xwh4l2ZtHApQ|VS{1g8MTgdB(=YLJunzh= ztsH>+v@GEdiyoTYZ2$h7-~44!ckYgJb65gPKV?NeqMtS&=vU-3D*7w9pAia>WB*|8 zE`a0QkvxEV>^RR!HgVY5Ff%i=?HSMgs{iG= z+2_sWIH&S{PouBZl0N;r)!mY^&|f~rPt}&rg8v#s|F;F8BL;7(V>a!co-;i?H^%L@ zOZ7Kjt_m;Kt?*t-{Vi`9ee}_jmbOO2(y>s*M&o>=(O(wo*+ygkqD9vljajp2&z(EB zfAZv0r%yLWN3Y+w@$kbBUurh5+_1 zaPY>B8-0C!=g*&?GG)rW_ue~c(xl0gC%4<}0|yRVy?PZcuV263Y&IWy=ph6WCfBZA zYqeSmXV0E}>C&aWd-p2GYWno)SZTr#Zr;49u5wS5moH!5xpSvis=w@IFH?>K8qL9{ zhOS(?#o&}#`L0JUtYxADu24sAWI9YXIrYR#l>xRAz@`P zKity;1M8|Yr|Z?LOHi+1o9(%3?jI|E$U-zSGEz)B<4yj~3&5n3-FXG^%RzsA+4ISR zPEXH;65@Qp_k729{QP5&`Prx#&iv9qO@H-ScTh8LdCU2}KHz(KC5U|S#TOyq)vtc_ zKmYST!^-!4-}l{p_uU2$g@EGA{K~KV3NV51J@?!Lf2U5J+OcEDd*1UNKS7WHDBQSl zet)ycF=5PMy zZwLSy-9U^F&hm)Sfa%$@XNf4Dk3RaSw?~c~QHVd{=bwKb6Qf9V12hEuzy9^F8}EVz z3y`TuV)yLX^VP3@b;gVthK4^G^C_?;a=UizdgPHumMvTMFaPo{@-Kav0OH@C zB*A|&S+@(McFRDL`FdC3byZopP0*}-&W5~bB^ex?(`dA7)@G0E>3tAin-9Z{#&%QR zZZEI4jsm*sh3r!>UHQp{ja~A!;9T$N*`&p44&0x$X`?xL3&Qa5!O>BpdFdE$ohJR? zSV2ZRSjzr#eS&s3`lI_RKdFXsMyB0f28z5BG3;nTzGZP9Ag6lIu(3h z|N7U%H(k4W_3G27Pt*6*B*f5&3VVBdEe=#Z8rY>@2?lULQgEXiKm(D-jvec(q`HbS zAeAQI5+9AKWet&6<=nY*P$iFY-n@B{1YkXS^r&utwVxUSN8Ip1?}?aDJ^0{*xZzLs z2#o5I9(ucc`Es2kKZfI#CKP(LV#NwSHAf`$R{k}wc@6ZF2TZhOyoO+;=#`KVJYV;^ z*L~mvANbDi{LXKD;~T^>3!|nz{jZ?ik%U{P3Fyzij{0c9aVj(ingz$P()SMmedTx7 z5t5;*yMG-0(wTPKcUjSBTyM9PL;vb=eSf$M{d+1uP_Wt7h5i=`{g#(jEhF#i@Bfu% z^J>O5K9uC|fB-bU`Cj?85`abDjQ-vd=;;1_5H#}A_cPk-h2G6fh~$%Ki7>k1ePfj&`YV3xXC^msf(n>eO}w3`SvBh74e+7H-TH z;7gYPmrTZjf@Ts$BtnLR4rui8#~-&+ty;ATnYZ}su(4Pxpj^ot-}pw-1>%?(h0cRM z86akwAVsQJ&5YuiXP!Yr2EP2|FaPil|M2$h+dIgJB=qQHnMU1kNv_eA?@loN$~tg9 ztgOsCiGWxgQ`^iLCy8{uA4OJ@md6K3;m?N{~HJ3p6c|X%3lfktL+<=pI?F=-9N8B z(8_h|Ub%if{V`l?-1^cY50odJf6MLSM?TUsG}NvHf^ZoH%ruXhXa7*N&;t{&#b}|M zeG#u<=vA+Jl`27%x9|sZSm6NW;i=qHbxgcvM`0412P;q2VbQBU|MNeGNSISaA`E24 zs^aa^rAze$4Qm8}4iG{Duz2xe!iiEEtE4AHR}})o5W+!;uBuDSG79n&e*<&4f9-2u zi{475&|nE$)*mi}R2MRfDWWBxnTdk3Z^mvlLlLFvlEn~D){wi6o1)(?S58}sl|J)Mv zrm?MC_sIJ7eWgMMOLA5e?q6N5Qa?5Mfe&1rI@P>F3F`tmhAX5mnWFW$0xgZrl25MpGC;q;vDjKCqEd|i21hzP`R4?G_ z7@*^ZnA_^778fmA#5-q{)BTzNW&H$Pl$MbgKN7^Ng}LTrbc3M}{gD=wZ+g?4bOZBL z%qXxLt%Szgl`B{3j}HR$3?U*%dW|9$+PmKME`}VbApF5cw;1#ggN|b`Ar0l93azDh zdf|9m&F`$5yG`I2^jG~v1pRco zjd-MF8c6*uFM=7k)lebB%u#y)=V{7emkq=82S*bh%LL{a?%qO=#er=Qw;|Av;h_Z7 zQckl1YnBvJK?blQLh_hsXxp}J5bviBsD&XgEnK+J&wcy$krq`@Ml5C0L}X71wM~|w zGj%bf=>b=t@ri$IG1;# zX49rk8p^1}3Ap>D=&{EhTf25`<69*?ZZNCz?*YScO~K;2BJ&*u@ik@l-zaE4SW`1v z(7dnEuj1C&{WV|v@aaPTaL`{`?_3x94|V=cY<@rcs<$`j-$^G%*otjjiwN62(fwef zu+a4?=4fvhu_A=d#dDmA%19r?w8+EX5u^6t{vL2VYCVl_Vk<}*|ik3 zgMPC9+Sf8=Xgjcg0CR74LGamUpEY@O-qcFo!rYoQYlen~AcMk-ajYVwB7+8S4_4qN zuK*YLgiRPZ;SY`xFc61+3SM)_2vrOly4hRwTm|xgPb_jN1{D>KkHtqrX(D!)MnRvD z$opv8<-G_`L3S8orD^J%M?n(&^i~ypj=w9#S|h-4PbxBsa4-rYr)m8(WULIqha!P& zEi{$_a^=;;bzu688Efay|MZnBuNTFtJ=gr-F`X<)h#|f`#`pN#-5ofVB+Rc_>l+x@ z(`Zcgs>%;X_jg%7OAGxMN=Op_xeNm(Z-edj%|>HmqtQYCrl7x?+kj`aGFHvaDNCVM zR?}WH*k%3S$tX}(%;P2Z>weN ztY-~T#yG&Ja!-0)T^5dtdIh8S=oL5uBIM}Y&vFEyfR31u5C*Nk z|NZYLIXEPWYFr5rkroQx8gh3fjdGV0wu(=zx2k9zL)|8FMuDMK4&v9ZUoY>YeaClv zho!*c*_kq|v`FfVYsz!ao!z>1&DdCfL2!>TzC`y&B^4a!);vb{FPPx;5ERNEPU*`ktp;DHB%twif_@Cs*DAj~Tm zg<4Pi!98$6<}IMZeL|lS94j#juM+6gjdOkibN5_0W^E!KNbQ#D-)FE_KG97Nx)Vi@*4xANnDoAW zU0Zp@6%V*XVz|m< z1&z@S^%X|HInK~5qi{nNzAih9@5zS<|ys#2{7@ zM;Z8_goKcRIIrB(*y5fEL{gu4`l@JRfp`^Jib{rlKcmqt1>Qm-{)ya65JrTUTWEq{ zs0xAV3S-&ZH_$@@9Gl0}5{FEm!%AM#heTjAG#rcqe+wERAqFI0&$Ii1K)g#Wqf=ra z$jD4WpB*7FmVf%EfBMpwzVzci{^Rq^bVxJq*>?nKjWPbtrMOS`==y1!_$ zxwd(1?2nnX<@;uI|Brlt=xiB^;l66JSvp_=^w+Bs_4Yu0opk(tt5-j`Y13#eh)1hr zUM-P1f6{B@uYUF8J9j?)^wTEKXu&k@AgsWFX#+c00Zm#NuoB>hD*96NBk@7>C{>KT zk$7n2Bu*-o31ON5Yi!{OTTc}|6y__9e>Oe0)cP<0JcaTHa^AB*VtoYq=>D1h+U-AV zHvhQxzq)ZRFWq?okb?dJE|g4twPQhlA^s%6D0w*1wafU9Rja0K-kh^m3yoh%HU-hw z2K!^@&oki@1~FTKsb^xB)_v+9ZsHjqtrR7lpk#K$KnG$xl|v7~0F-QRsSfdo!3cm7 z+e!FebIU4aA5F!CP~jNQv_(R2mdR=pKGrCnhM%}4EW`H5I-q*`mQCVR#6P2eZw3qp z27jY~Nd44X15rq93|Y6mGA=foOyZjw_0+?8l@KQenkCUT0-pis4KSxa-tdMua13zE z$0TO4_q01_!Gb?)nKb?N?!L_e`wUs&nO7oOB zy`OJ=>s!sM$%Er~KR{saX}FO}K<0DgP>}K%ODz7lmK{*K#Q3Y-hszmy;Vq|5|0gkVIT+jr* z#F4O)w|!=ect%t-ZB4^UXXJ&ve1}%0=y*dQDp?8$3_cA75_4xT@Jc=7e?K|g55>rKq(DOS6Dp?_EX4cByEzU!`&trkgAezvBv zFA>k6U#$|PoOnxYXLU@2YU1GtThL$4Ne3OEXt&}1H!X=>&`*-@&fLj;fB-rv2R5L8 zQR(urS_g8)u36rX6ykLJN`0C+@C0%b0&HO*LJsgotRk7b{Zb(&Bsy;b*>m5jZ!XPhlFb+hB4 zIYW>)1ag=T8%6M~mcGR`m-*0#K7>F%8wkWECu)(k-tYzo{hAP8=%1C?-MBhy=TE*g z{U6nl=06x7{_WAx7ske-`=#>`To#TL%V&)qW=}VUZsj+Y`qTRuS_`xDRZ7tKpJ+D! z)|cvn{zTAsXYPyubf-Z}Uaen~;`NqlpOW}Mlt-5gz-<0GH*hBjr_1>1D>d+V&Y#?O zUsS%iffd`F)I2;n^#w*|8U#&tjE4Z7X)Y7Js4fIhgcP3YR{uEDR3LAa*r*QQq#^-O zNFo3+3^=Ti7vo-uT8Z3~A{~g=3per*>2#{)NOL%2k~3=f%L5{X4R%rddYeNN^sZWz zt{!G%L0!R0y%ncQ6cxm+Aql@y0YT1&-iG|G9sT*uvpYGWTQynl6R5q zGXse$%;-X6XOR4`!E!n%Cqz;aePkPbnDCu&P?teSDd+$XN`I&f)wBL=nm*kI?O>~w zAdI<~U%NNWF5GvgnCH&%H5^o-_zL|JJS5)V8@r!UOMoQjd=eZl3Arceul%l7>v0AkQ8z*p%5G7#n2k>DOEG)99;6iX-d!S^vhm?m;w>7=n} z)bM}yXMgtFzU|w-_O-7WqvlZRHV(0u2JgLh>b`x=6DRi6+$9*lKPJ$(l7Vb%#wa>L z2^QRce{_@y_(`2E-M_UYA-aE4p?_KsUxwD;xFkH?Y_4xMM++YQ@XWKno~_%U|4y4g z!`TUs*IW7Hh31zt0+8Jgq99p?@ubuLm%EbGRbgcgge*x>$ zt=1mlqoV)&W|IZKQ*+Kfl~uF6%z@bbkDBYmdwPuRjOwPL`;txOmv8@kjgWvLao zK?!-WRrG`oW`(n?04wL@rc}~;j_y&t5C+h3o#WKz+;Z-DY0J;tE9h8dswn3Sc%?sy zZ0cv6lWbiHt%~6!j7vC4jy}Zb@9YogQihzti&;+Z=V zC=U-0uNc3Yx3l#3<<1^u*9y>qA%=VCk+HFD4uiO#P?tnqBid819xY%TZ?})v|Ciy^ zxE5vk%*Yk9cH8NO6BJgL*6$Vr4SDp+-y-gL=kEhF>Km=R^K zOy2b9qbGLlQaDh!zf>RMZ_5LIu9%#7#Vh(3EC8Ksb2{5dQDrsA^)BeoZ9`u>nC8;S zBWC+Xv#Rcu5|>3dvY=*yy@HhlZ32{Vh|{bjQPk{7DP%hZ=s8lMMJkpIcqX6@SZe8y zpH?bIZU~#+c8=@BwpL;k>=W&30^;F5L0(qss1&Z#;O`)4B~|57q= zvHZ7Y{k7_^dj+r}rw{=4I3z-0)xMbH<1P)o(QRy(q1)|mjE-_GhH7cr(iLJ7k^Mv0 z?z-Uqtp(uLX3u|J02=?bzgq}E)WgEs^Ge*HQTZw3%iP)01TU{ws}?UF-nDD21mnv1 zndM%{f^ORoV*jsQE|ye(RVB{odI-WjHlz%RtB7jO0K_U$&%4Z&RW}WNPyT^BCggtO(+*Xb@C^E zkt$hmqDO35bSu;Z3G|RHb(GVkLXh|G>%mG{5c~*nYMG2jdwY4qHnXfjb2%h-xO#gR zmfdfa_{Uc3&znv6LF4ZR?yWBLr?4l_aWM$+{_9bo)c+5cr{UXE=3t=b>fD%~fc|!w zH?2ba^&0cV{DVRjJClx;rTB&tBp2PcJo_wI#L&-KOKxs9@S!u2^(ancAnPst7aecn z;XXUZD(2jLs}jM$&}3)F6v`9e_bPs^;AU?3#%@MQ#cCpZ0!0~ieF+J>VEB*++x0nDwcyli&!K9WNM^=t?CUFyFZ`nN*AB#h-O-L_iZZz%wsGYhZH7I!J>97M~t z<(uAC`DK5VPk{eI(f3vi>wFBJTkHnp^ftCD%G&8hf2+_xQu)4819N3!L6vkpQvTMA zDs&DZD2#!doufo2qer1X=g;C6hX(TC4`uEvcK*2@WRy2|FiJY{``n?C$q}8Las!|bAQA=n0Q&epi6fy;Td|`s>>P8-nv?TXzK;&X!|}{<)K0uBcdWIACOZjp zow}`<9th@k%b8lw->sx1AqS+f5QT8o%8u0qe>8<%8W0>8h7S6JkS9h)PWJWLu-vFm zT{b;z_$g&I=!qEI8vVE@rJ;w#h^I}no{-qcVQNJHy6P0R-dR?PbfN}upI0$aa=fWv zI@)!F^-yii_Lr-ACg=~h$|RqZe5(ZD3bDy$fI_^aIswa>ax?HL{Vsv&t-mySvj=hm1rMU!SvnL=3qv$4Ryrd>Xet*Shj%RpL(xv_%sr;pq`$wzF zwaVWMphf&_{k_C7>Igudy)s?TX|?Nde@^?9%Nrly3-5Txy1)PXP6+k-`1OE4ELZq# zOrS4*k(tl_fjrhK8GyVP{V{<=Fz86rnTh1;MJXnxi6-=tpk}`VXN)E5z@-{x zVr=Hj&~T!4Lj@T}tMlQ!GPK+c@CuCl)Hh8FjT0w$K{IOg=s15)?jL#6n;h5v%N;wi*C9e> zkI%r_OM!JRA`^g|Daxq|CHx1bm%|jm-K^xF40BBk69dUgC?E8zr~|Ek{^vzRtCZvU z(pK`9YENKU&|hbMUhSV1(@O0YKUmO`|0n-^-N?wkCnQ8jLXadl zv`~T;K9jc7ad&rjcXxMpmyS!P{oGyaf)$bwf)tC8Kthb<&2RQ^?X%wMqn&mQ`Q)yU zb-20jUHhMXw*EJs4Mer1g{H^xMnS)>OxJ7Hf#3iA-{%FZ+o-i0;Q}ZXJ{(*+OFptU z?(Ng>zPq`*`@3HDvYw+yH@*4ISE+-+V6z}Sq)GQ{UrT_xdF$3UY~4ymKgu3cRL`cV zoeF!kFIg~jCF43{S|Thoa#*QwjB;>eY<|$doMoK&mDk7nx)`8WH5d)#>`L;RlQC`x z*+0NB0j+B>6Ygkectw5v>oA0{0cr(#m4fsfX>Dax%V5_Wr~aHc=~HuaJHykA7+YEU zRQ-H4(bxH~Ww)i9ayXW@rEzU+?2mgJsnK?7=YU(81O9HbHF-R>E9ylH z3;O=2LW+!rmbD^Qo6WK3ox{TuOP8XM5MJdC@k(?Kf_SQA)M&LJSp+!Dxlz(`43z3H zDT0NckK~Ok?bp5MJq+%>_ukL@_>ceTpZci_zx~^1$-CwSNgooofGzULM~{BO;NWB5 z_kE|^+Y#yj+FDJEzf3W5DbP&>^!cwL5une+;9&W@%qle`qHEZC(KT5A3dgMR9StG4 zC(+**W}d>aKaMe^a~z>*em@+37$nqDpqN#``o_@L6NB?tj0Nf%tH&M?F7oxr(>S}e zJ75MA@aZH_ImD@Nmni|MXXXHTk9Y1b_bwZ#v!}%iJ^;!oh>a z%lcx25fSWa@9JxM+uPnosvJ{7L&DfL^MQx~iW4RWJC*%R7;~pBh~ts!x#nkk=6E^} zRg8OuiinS6nzOQTbHHqt{Q=g`hWER<8B4BK?Qtu6u#CU3rR4{MJNlc$+Wz5a4e>o; z6aEm4>UDKLRaf`PK|H=MaB?INZzA0&=g0+W3ru@Umo#e<{ij0qJE424@;Xms zhI9W7asT|LL2G>N)pRS(r0?~5jO~tpKIWLt*0~^yF77{LuF^E9`7kSZZ59DG#Ws_h zb+Lo7$+^jx;kotJ|y!kH?60|LDhm{2PDtM~{5Ur<{sS zCjp`zh+B1_)6;Y*o|1LX#?)9Xks6x3qMre>-?5Ga#+OM32|=mn`fQb1cs$W>p3iNf zKlV!yI2RD=O4lDI{O2||Q+f2<;K3J0?w6VfsC#i1(LXMc7UN3g?NKJi1|~D&Q;HMO?ts@B8PTzo zi^;)9%Y68)fBUz_UAsv70SEWd9;&|nm+rXZ^{;#-Hj?QWrf@p>)c}b(aPNfPmMi8re^Vw&P@bH0dNxRVr_;tJ_pp*TEGA`n%(YM;?w_2K2N%L05LzxOgqANlklSIhb zAGdTq{yGrd(bmMZk!Sq$Q%^~aXym9Q49g0E3Iy~ql!=uR&l2=bgB;|RDp1XZefYP! znqXoXlt2B#@BjYq{q#@2=eK|RjSxAa%=GoOU%0?MNvjCT(Q|MWe9|X<63@y)_}18v z^why19RRv0$Ma(poXLZ6B|(tcrl!+6<{P?kSOq-Jr+aWRpef7Mj4ZaPBp(YwY3ZMu z&#Fu#T>&Cve45VwO-)-#g&P^PYs(aiBRw53v0bVz^)^Se0RIZ#y9Sh)*?=`KTDghq`5N^ro!oG&y1)= zRrMTjFo*v%PJMgw$&l&$ubCj;m4!kh@o?KigdBSK;n6I~WqsGfEysa8xGQ5T@t|tx z*Up5h3Gk7%RbNkafW?77Cx(W8@Gt)2+kfIGuK(@d_O!K?Bo5Uc=D(R2=7d}KBa;Av zoU{(&4~&i6NhXJBB)yy#u=HWh5DSm^>vf7B0*Koa9t$CuDglqlfQ{j!OR>Kp2@SSo zmET~j4=o|}dKrgyMB@hH=eO#viJ8+axnjVVz(*%0c-G(C6?ixk-~2H#mRjMZoGysh z9|BQfI#VDbpBKWE_yZU_Sv>)Kj++9<-IG+@>tQCMj`@89^Rn6j_DP zLLZYa_-&%K56}!by`rNmFR5V<#Z}1zXA;Eb0uO_#F}{>e^NB$1tpUer&>Z8Iq!_xz z^6|72Ix~Z&J&130zF$vE^=zC%S$vTn%?~}6jzc4Ym*sAv>1mX|DE>PC3?tZN{myJ* z(B#Z>a?5O<5!|iD1mbQt#m&ss_Ro19g6)PSq)%JMKXzmBEVB)y#pa?S>wz56jODHGY3;#^x2fcH#Ur z~Ij-Z9ub_MO7|q(;^I(Tb^$!hwA`->XowE zxJqYCU?N*UTn-c}G_P=2j79(Hm?4dtlra|l)&N)quO_0E+!wNVL(1jrMyL|~yE7Dh zLEnt%*Gri+qnj{q%>RF}`j~k~tQp+ox;s{lXUa ze5rA+iJ0pwx35I2vwcSNoO(x`NF}d_6?5Z81RfR}$z)^-ps@0qNiLwOm6y#P{)+J9 zE!H~Ms?qO%Zh*6o>>u613+W{^%bG`F(koClR*iatfRG1;8hLP!SCr|&VLFIVc=#_y zg3OBMYpCusF69sf{l)=IUb3p`x-eBWNKUxXSQa*>pgfo8pOh9P(cd3q(J#GRTk<}v z)=o%jQG;%ve>UWACBQHeNT|fA&q%fvJ5PpL(r7qBCv1%0#rH>2hCJlnYT!dUyG&zh z7X7LDY~^>reKMA(I&*#)YccDVx#0@%yqALNMc;3OsO^*49~b?;c*^V98H&)Iv4lMY zIbjg~n4>RaA#cc~+=xAyFo-v~l%E0p@?Xb!%^#s#t!`GELI94r540Uz0v1dX%(9@_ zGI^Ewa@QPV*#pa_IoKG)tf-jFs*W6^`0%rg)rd@wBO-7y0IKGr2X>CHl7{`mf~78;_-6Ti)K0xU6Sv&ks*&4Af{rOk%WA z8B@5i$3le6#5DLkipAp5V6P$1^!AwgFXaha3ElI%R3z50CRL&Tqa+fnq*!?N>ogUr zBQ}Hr>OOe2#9qHAkINJ~skN*S5bGw@np=hVCv?D{EH~8}b|xtM**}KC5TaD{Pi1cJ zl|+AC=0)#K?zi)SczHjc=6KN`DDTed$AN&p%{5M?6~})PM7&IDS-|m39N&^@1Z&J?N^iJds~)ZoMypqnUD1DA zY9D9QFF%&4N6S+7Z;w-4(*`Kv=RkO?(Fv<$iZh{6^v9RZd%i^ftwr|7*nr>DG11>& z``TW(F;MN`Gan8@g;&Q}zAP5aHJ&u?b0#RqNbN--HOtb}24AzxwYDo&0Wf{PgjZ^l zt3DD{pcj5I5>2}T6g|-L2ImHdceS){F~>77+Z68*wi8dxf*9$kImRjiLki{vsXO`c zxj_F+z*Kl#)?7?&XKTv-i#4*}*f2ApOTirSzC*n~Wq)_^K&JdXSCw=DFQ(J?T1?7q@z;rGb?@Z% zLgs)!=fOHXq9ue$Ziw?SvsWdpWZU@o>Zk$m^;pp-tB<}5Rs+mI->d$<=W3Vm(Z4|s zCr2H(Y(Nn7>%Iiyz}j3+J^GU5nH`AvOYEEp<6~g~LHy%Pvz(2cK3-+Nhw$FXg+bwd>~laKNhmT66imtauuiU z75$lr(Ifg}yd`CSZ=8B{aj<$xPJrcs{&|i1T*8K{(I5YH-LV2@TedQut7#wmf}vkC z2e@?Ffpo-mpN`92$VFf@PCCtXLi@UF7n&=tS!QtAo~6XHhQmA-(;gXAZ#DJWAN3o` z4?R^8^Ae*=>Zp%jZg-O!Y-L~yw7*N84adtlXNjEoBNm9U74(}Igbgv3?1+=Le@^rd zw#WC6Cc##n+D<`#%KjGher12A z9(56Whqb5eEeG01tB{b&{SaC1krV1Hm%-z zXF?ofjmJ4Dp&SPsJCwh1Kxc**Do&&fEpJoyXK2}C{1a7`iFU6k(XLyveSDvqPeFf1 zE!qK+E5Mq#jyjhu!x_dJQsUm0=NmNKohHlVO)`ILtxs~_AK-Uf<>zLXgi45z+hSro zFfoB8XAQ2QF4kp5zWxolLG3}UPteAXG9H&{ppxKuKYFJDjqc=KRI9O?UaAXRK>U}c z+~6Mel&i5>ne78x=s#%) z%n6`VxuE}QNpmgex4g}iS7%ksu=m{+b6{94i0aZv z^q1gCN)HnvATr_I^E!F=5}9xq)5BZ|UN%uPRt$Of(=yJpf3z9FUjLVV>6eo3FP#bc zJp<~Y*>4>^d!#xfFao%-j(9$%QE`BVfrJqsSv39v39 zIP6%&XX*f-Jgo&BV`abSe>&i3IN9p+u9W@7138?sxS+o$iFNz+eoCyHZRJb%n${9r zQ@Kj?cPIMm!?k$IX|UB0M#}!JndW*)^rzmxGqa%kVtl>E0Jw>-91qVcq8syyxPRSb z+d{ot7taNG#aomf$FAI!yY*j_4G6OKlzhC8Fwe%VN<6#(tK&Y0l9!u#FWtdtwq0lrtA^@hL2_g zof|FTNO`|B#%J|@Iu0zhq}AINf>~XEnK?g!{y8IGX@zbnnuzjO^p}F0K>vX}^}$5{ ze4>A4%KnZj^f$!Su29)l_Ul!h$V`WEi+$4He>!KvOnjGF(4!gvi_=pB7v0Cl^SULg zOE1I)+!=fB;u=D8y&jvvU|x)zXS^*2CjHtiLg=$ub#Ce?W%A%aouOnGJ9zG`Zc%?; z;3NH!ANdh*k2Z)a6YY-NC+uDCQ7eeQBI)_BMe&FU?j6-=GB(yvJ_3^pt--XTIKYE_ zwng(1{niBYSDK%#&z>%0W;)3C5#xr;BI}O*%KKGJBoQCzZ%@r9D`57@ZYip#t{ORRKx>%3pE&b#SVkO@HMErN1psuhS%hD}_M<=gqu?7pP?LZ7ZQf34xyN&0J)DZoB@sU< zo+8`fuvr9b%@D^kLy#KSOQnQb+0SQfdn5Y!eiDq-YDU#?l;4T|iawv*e>Nm>Q~G&Y zV**H&zcj?0=$}lK{`p;Z{J7;8nsRpcG@*i=5%F$LBC_kh**55jr*#I>-#TSkh3Dj z7Be+x!d!c*DE5Ca@--rFY)LCh3;1&U=#9K88Wa5|QYW7c#P>MCl8~!ipO1xB2G?Ys z%_V3yPH~W(4e@nbJ$L-L4oqbELPJcC!8pcS>W^a%3;q9B2&{I+$h1)MQj$u#i|dXD zF&BH5$4*`w+))zz>zhN2@mq#Aq`MTK7=@?@nz?F0rie;DE>-A(ojYIg*kd>jABiqK zGan{*5TRG5{d4;|KkxHCkL!mG0BioJYZ3bAT(J&0Dc>cnG9F7!9z-#lFZq zC^J3H@#J8Q0lop*Avd)QLe=PZ73IG!q|+D=%6nTri+IuRei0w=ZAls57U&o8jfEI9 zey>f$U#m(Z9S@j_e!pr8xg2QTuFofnX|mUP%Knho9hp>jTG5%#)>$2_t^lh6MW%acRftX8^g!JZW1Yrh`X!1bUpw82Z>-0b{>X__AnX3VGRZRip_hf5-v)#a=Tp!>7x3B;omPTIeXToQ?;%tq`>5ESSjF4;!E_O zXRPKlN-$V>iPAGIHy!r$7l^)TgFajE!yV?Oif9MvlvsxIC?MSv8eX z5(dh9r3r{=8D)M8EwnTnF%hKpWHyWC7Xu_y;-B#fz(gd~PP&b5!Dmp%)K+ z?&m(-(_=pldD!)kAeB)$fEt~+xvI;tX^Zn;;12p`qF+2vQN>O>2u*wuZducR5S+4;sqO zr0w}4vl-uPj}T9p4Gq8!6`t31@zg15^*(_(4yAfY)~Eg>ak+tdDU?g= zY5pt!y)RD-Shjn{jZq@~7&7wEpHK;sdW0#m(S&ysH}Ephcfe>ydx92 z`7em)o6X#0w#ZD%F>3byHu=Z9=1AMN5)d$6&Y#%t^{YggSnl^TF1~-q{E3e=pe!N# z10GE!?whK}S#y5UuivgVTV#KFHY*BgWxrOhMSesbp3(M)WyMQ+R`Xe+ zDO|KE2KeUsxp@8yyRFUhynf(rQob$p_^&!T(w!S&=JQuq38No&B+v>nf~zd>G5Xh1 z2cx3J)p9+WPn_4gD25Yb*8%k<y8hDC`&Xx@aUo#p zIxv|D)f4gGb*cGy3ia@FvA>{SsU$bg+H@-odOBTCnLbky{b>P@#3?kgD@=fT(Vt0t ziGC-vlRG=xX-~unh`ur-XBHt8;8yLXK%t_>Lzg|9Q8m1h* z&M}t)$B2L10P|lZy^b$kIyRu^H|OHFZIAVq>gQpG*c#PF@SehHtQMpZQi|f0{U#bs zkBn@nRI1UhuCM0PR*n9ohiD4)o4;9!aiNkHrk0YeE1FM?jdooOl$TPnu0;P}NvsI{ zygkt`V?_zB&6dhFmlsbLB3|Mq3!>GLA7CO1c?7HQ!$0QjqBAN$REuqTpULRDC2LP0;USP$EHOy(!kzJLG0 zgCDzpKXSE`^wKPdYtNKJ+~A+{*khYEY`C^>pNW%fN=$DMYcJ45H&w?!3F6>9f!8Wh=mUVP0?B!lJ6kw^O{v5m2b80{9 zV-e!oC-y=vJ|F+2_^>l)cQmju>tVg?faLWljL!u6+tUVU%B+XsD$Qp+z*f+&@86Wt zkqjrRXsy@G$aSDO!^xc@({h6N(GAsDDwVCLP0iEG$1mf>qHeqrRA>2FrRUeh;eXp>Q`so59Mu^}@ma~?I1)tyF$@Y>?k)dgt46<_ zlzf_pV$QD#x$-Cv*xy^r-q;q$@Ff!YtKxGR z{7KW_2Eg)OvB{Oq9-kz?U9YO^U09A-f=m)}Y@wgtLx59~r=#TI~GZFv}EmWp!3uAwB^am{##{S(mho-&%p@ zuM_oTap%4A&NxOtAm0EKEF2S3_+N~kn237}SrvEv_JarS-nWmpOH1y$5OQqo>FL6Z z*zphlfX}7!gc%o{)fu)nX=@*mciY>GLH(klpIL5Q zOKiM6gra7g{cH79um=Ip0ru9l2T{c>XO3y{4)hzU!U7Rqs%3%0BO~iGTFy-9NO#0x zD3=c(ro(kB?Reug*sG>4#%wf{eDS^aPTqgN4aIB_224Q-{wPRFn0{WEF(yvxl{2?F zfR8vi?e6H5RyMm_h)$8`qma7Crb#C46kDG?oPt@+eK}#c7!??~Q|)N)(}W6Ym1|TV(&~8mo7C9P~is zW|jo*(e=m3JO_s3!@{c&ADM8BGk)PLH| zRLW?O9pzEF5Sud?5BklA4%MSUk!l*x(T2E|Mm&LN0DS-XpBs)H;}jN~vl?*Ch-$lP zn|aUK#hF0^g!G<0dpJhaa$o2w=5lM)0y{if+z4|69CH^!5ToA%1U>&X6BfJQ_8-P2 z$Z?`K42e?4E9}TrG$>1`^#maGxoqSJUu%GZML@%e&lbZ;Iw02p#us{f*F!It6^v^W z{bvgLGrzw(#=~*sTE^=A&IC*UN*bx<1ZFn``a5%c-bA{T%PU-V;&d%)K3cssX+`?` z$4k|+PR>-c&NvCRGx*Pzj4hu|e?KvsM=BKqc*c-hj5RFg1dySK*W(-I9q8smWEz@b z)BYp&#D1GP@|$;QHxvtESQdESs>Ogz8$j)0FLc;>)?G00b<@CxKG($rkyjUk?Ll+By>9lwpOM|x+)=spd7H)i}YF$~*y6 z8|?k)2R*f|5?D;Q0RCi)e2Gu=Q>mme(8|I9pm#>K>^DGW|NZqtLzI?QObf^cho|Ey zb!TmDi?TeR!e(mFxLIy7wh`7=yG&%BkCWLGK`S)L6uA+ zZBE2Dr3HL3j$E0tf3_;LJfXpzmCB`V^Q_izDp3vE0ImaG_jIDaB`xdLK)=_~V9%Mw zp*`yg^f&Vpo(kR#a!y~Nxs8ZeWx236g-bzuHZ~Ej#{*F+SuDrMaxD6RBU+G26aPC& zsHe%wi3VPP)>WW?N4}xakQw(JIr4RT_H3zC_K%DV#XGY)>HRSQzL^Z7fBn}7+uDxr z+{uMmYz}DfjY>VsW?+uklUk5jIyNA34x^SJssVc8rbYKNKE+|UJGppfG7_DON73>Y zOpcJy6S;04k;Pn$WzDcNq)VUeiY*l~eWS<=7R=xKz26h@c0Shitirb9;6cJDy#Zg< z0mf_6fp)T*%PoE@TZhaz*8h_k&TPha&2XM6l_&ak^u?zye^pAEi4e|*=j71yty8z1ioU1(!Gy04%tReS56d#--tH_qL4mkSpc$-GsXI_0=j z;}0Mb%mHZrDv=3Oh5wp~sR*|=#SoYY`uJN*c*{a(?}bUl2TSKa&SLr+zxi_1Hwgqd zqwX?S3@3nDgFM*J`mE1F`!9iK!;4O1zJxhif96bY5Nj#~2eZ_WO*7QEswu~%hHJu^ z5aSWo0dspCrwMUONu(=zHyKWD)1pmHBvXmZ{WMKD5ue?L7GmdR18p(BnY;e+9|u;ietORyoc#|x@PHGNNejYst+)p$Yu<330NoTcUGH`- z;xP1ny;Y@85eVk1REk_ixGBtTi@AU`Pl+cW;UQDSyrbe^EhKr4c?kGqTVjJ6k#D}| zo_h=xm&W5ZnQX)D^X_*ar=M}E76ZvB#U_Wooc%y{Lh+$mq8h0pp4!f}q>gJ;6_F&} zwb#sp3?rx7-6iqlHy9biqaKh)B79DbSS~RsZz2KpX~5_ zvS8==CgTJ*eQS!-DKo}{q(DQ^bO6Jj$TK103%1P91{uCHQLeBzqg~~JV?@7VSk^Sd zXJaHqRuOGqEaccsx0@-Ys#3@Zrpod|hk8dxyY%aaw$rAACEz=6 zXVGLa*_8r&G`iU}=zzDnP}!g8pVcJI{C?5cmgw(D-FwJn3MG+F)hrvB2@R*Ny31&W z7rN8P9xaH^b)c!J^hP};B*gZ`J81Q-+La;e`6On$lG@d=#fg7uEpfV*umN=S=;Z(b zCYDL~!`mus#Nd0WJD{IHJG(`0;_mhr274g8o*WX6pJW`_Cu(*=(ZPlT76o;uxFZs`)e*hl~Z2 z=G&XwqrYGIn^y1Y8mB5ynKr^DOJ|IA>yHOYMgQfP38LS1pd}~RN{%Uu0_(2TNyCZ$ zNa#Yn6Ny0;G+C9pB`z-YeU}!I(jEe64tc`GEk#=yAq~9^w2}V z2y4%J!s`AGYC&d_b)p4jd#U{??@u`sGUl~C#n{;lVUEtv|I*Q;KTNK{sZ&o~xl-8n z+EVXRatIX|z^ z9S8vFtsac`CHl|FTAt$Lf_-%}yKH+va3US>&DAXe^gxM2#RZ`)&Z+t2T4S&F&cwkrmp^?;i(m#+ zEMZo?+r7IIVIbazDenxB@fdTwT?qgcU@q(Y)z())q_i|-VgDQ8q7u;FeKYFVS)^clr(w@htE(mhww)4Tn zePslnvQo?{g%XM56o$s7&i2&oA?yYEAAkIDBXO3bxoEUBH@Elo4LCev<~$nT-jSZv`KqMtsVeke&H~VzzAaBv zh9cI2Y2>{%F50YmGxXu`UeSrocrkxnmDi+VS9#zfg=lrZe?DmNh_9spWGENzap@+r zG;|;r@yRSXZI&z}oo4427QW-~;oslC|6lw2k4#Q>+RrDgh`O*y_WaqOE&1;6Uh;}p zu#V}`;+nY;eV1r!Tnru|k*6cy#JJMO1zd8=tOg2frVtBYHPyQ@HDdym*t81xHvFoR zL*g@48)PGDP9YnUgl1~5dChB({m(!@p9TGG{r!WFJOcWK-N{6MU7&v<(67&DJP=(*mw2@0I;7L2~8lJk3P9%_h%fVnwsE z-_#yM2lDc!X;I%3f1?(I|1`Mnq@QP%vfrd_J*gJ##2lJggcV>#2F7XnbvD|X%Wx=# zw=dNepR$+f%qbcF=olyr*1|I@WN~7Eyd+!3WkhF-Jo!}$e(YKYB89RlxT}`3pZ&T7oaZcYeu{qVIwnHyIfVtvp7%79rx zXc_C`jcS&C+)$bVx3u?^q_SAw-Sn(8Nd0sci z6aU92p7@mm2k!6h|Fp?TU+28^D)HP%;N1F#H;jM!r_b)#!6kXMmb@_#y9Rw3FpZtt z+>U&vC^M-0_On2h$3ZNoi^->akIAHf-V|$d-SiO1C}EJ8FSH)=o4)CrI0jBdo`V*V z=;y1@CRdT%iq$(d)|pi)CzSnZ0newG^K3vup8lcGUJj_D*p1Zss$?x+o{jOg2GVA; znsHT;{h7IN+J!V@!}FQHIU;puB6COwtL0qjVwx_6?51yOn>iDz^R#tZkXUe)*%|0x zndrZ+?_cygBQtp;*-!adlx;SZF5{YJH1OtJ2Xy4g0igk4lB27E?K`EnXSwGvifd6e zN&%E9WmY*k6WBNalw68-Ot*znF)8JkGNyDnk8A5sy2<(K%*QMlyN`O}?b~}=TUTWb zaZ@tipJ&hP?4CEh>DnWYh<-20!K&D}7vs44g19eYz}&w5eU%9A*8~E=*ajZY_aN87 zH^QRyLRChvc?!eAzcy&Jv%;@q6P zOZ9QgJC`hZE$CTT*b7g@h7Dw5a7R+Q14=Flxo#aWcVQ-WI_11mrf*UQXEYLPtQmf7YzUnf*3zrDqx8;ADhNd6$Uu!rRsQ1gD#@GY_ z4Y*dV?s^<8d!TA`wc>eyugFkfOz}o(PJTDC{ zp4R@wo5Li(<1hZ=(eMBM^F2KlH!{PiTvHhnsX9hYX66ZK$6MfUP6sQ4nw~|WhQmyJ z;I4andoggCHYSHsm!s80?EHJa=X)wun=38nW$7Sfcvry1lJny8Vb3jT0WatpwnKGl zJC_1>FIDr2hjvFQxBi%z zFnd$;IqCXP65F)XQd8~7^`Xrr$fjGAymp8EJ{}+IG}cq2_bdBNye_lzsl`c$-W;df z@(G_X9RO9-A+Yg#pa)C-!TV$E8{!8zQ)#B#&>L2Vv z>wW#}ug=UIiBlvlm#RVJ2Onj`KlB6~(8ep@^Ny`1X(!mdA_!``%jhtcmvfBfkcar}5R zw7n~<3*Q%1-i=_oqvO%Ky4^0GG7ktrYcNvR?@iV!HlRFP6@;R_n6@%w+JjTfIMHy{Y%RHl^%mY{K25vBNey zxu>UTXO7CtOGS+_^&hN|3}^RGlv)I#nJVTL%W{ROz@LaQ*&Bv8O;AaW5R)IRo^c%j zI(!5d`b4~p+fxr@9r!fNVms3WAX>96$eMhbgBhNj4`j2gi({_Ee}DVXq3^%__Im~f zXgK=P_=auq+2zU(IJtiIXW#ka7sKw0xx$s8x@gHT;U#`UigOoSFn7Ukc&xZiNKAc&mbdQB^YzS&;R_-0&vbD(yc3FzqZZVbHM=1d6S-mZckaC}Lk ze*k%JmRlhE?sP%EKiWC#jDIXNrM6@qxTRE|1{T++M$~M&sEU^PSJ@bEZ)zxKiTyEe z1Ep%JdM2OHbDgbXw&hGXZeoSKxGf#d)T5>GzIyr8LY6J3-k<0U+^X2td_3u9&Z?`l3%GK>QS>nlddRfD$FTOpU;f-1eOg1?l&x2M=fW@#Ugum`vReQaOd zrj`O~1I)%IT>?AFL^Bu2RRvqK|1WzBQtrcIgPoCG96fem&JJ3)8F?uM~-~s z&YewX&yv0E(%MuaeGwt*dEkMg-Q6=?U2;1!)pg#Zv!U=XD@>A;$BIG8>9@2^_a*y@ z<}wE?Z0D}I-=F!JpZVoq{^erqR<8yomYQ07!%cDf`Xx)=5@6aLQu%t4ojjV|{OdTt z)ZIX{L?e^24w!oI02Z(r%P~W?OOB#oHkVV`Z&T`Q(YrINk*;WRWFGGRtY|rDOBmaM zX?({ApNh}6rnK7RJ4%xDu5_?2vtl8amj-WODt?lrC4D}|mR$!#f6jyjf8SK-*XQpk zd^HmjKN8QbA$H0{0!**QMSlW!wPnlo&6`z+Oz~%oqKpC{C7_iY)|Vg}m71JLixBiU zM%=E#c*snL?K$WBIbFytEYp+Hk0o6|i#C93prOia8ND|4i|@{usDItt`x85MEIoG) zHZQIGoeI-6 zT8=X#T9WrF@1+40<;IpBm&}S~r9}9qSOPVjSFBgKYK~O=SnXzrqu9xdG$7r+~{bpBZCS6zhD;Hy{R24x_NFNYSsyTqz zE=)0o!qsEKy}V9py`}~jKqYO)py>xb@B#BBjRWX8HHip@SlKPjo;4}gMSLKlV{~6C z9#5a_rK0!CbYOyRNtjK4?nH>avkMEq+TZ`3+qSW@CDwtK)P<(w@vwwds+8)s3 zbaNfq?@!%WXyL;qWlx{ax_Gvtzoqzm;;=o5{xP#rYzHd(W9O+D>t@hjXLRyl>{s?5 ztwMia==R;Yyfh~F`njypIVmfI6L)Fpcx+Z($tg99S{qLEXUCi6ivxXr+^pMgjHT_9 ztagiwydK98*py30x^UCK^c8}8g{Xxlrb}o8NUSN0O3!DssBY_qOifAjpNh$bvyePl z6SH*W*~e3nHk9(d)c$o@;hN~L*O9ug@MkAauBWEHy`70M?VTE%G+s6}wf)zBeH~PG zv7yIv7bZ<`cV#lLpL?`vyS^YCWjqJDL}jpIO3*9O%C#-8m=H z$X=Qri!EtbmkPDfG65zPw4z_lM@l+nzqMrP@i5_0AzimP4^Dg39<<#~&dg4GU2yW8 zk7H1H0KMKrOT~HR{8s=WGVzC)8endWF;O9<65vW4)2fg;g=K9DB*hk0BvZpwD>dqlrETE2^r{kx3E z$9St~i5ap=^xGCv^h@)RQSIS?6Ppu2f4{O{%_q@cCHpTa`*qMt`o0LzcRUraJDuy# zY~1HS__ogPnYCASqFkEQrTXiYwczms@brx*Y&882HFL-ti9Z$R^m_ zrtSgO3QUW4zVn^G^;^HiI#3r|ZL962?p=gU^-|e?yvY8HcI^@U@s?i_NE?ag|Dr_y zfMJA8s@s<|5Qk&t=;hSG+MN^3JPuFMXflO$`q|}iINn!XP5?PX6aCrcrl7y6s-B=Z zO9xk4PPRD8kK;BA4{R=*Woz@9eMK8+Uh3;#|9ZM03J6VMK7K4VdkdHXZ2(Am6&)ZN z4`v$E@JoB6kfMryBi{}2-yv&&GVWVs|AMmLr>`h6flBmSc9hHo3tTHC%+f*VQtQoF7W`1s>zsXX*M1N17;&Lne_z&TqQ{HEqV8#0V!e;DD$cZ#Ar(gDDZpn6txGI0< z#EJbIHk@v4eU#T3H~Tgc>Cyvu-}LnPH@x9WZ!aoMX;;c!7%=8$nbJV~eDug6;Kf>D z-W&TZ`!X+@|4O?;OK@}~>|o4;U;DLRvwuQ?ei>I5r(^!c`1T_E>(u+x%UNXqvVi7b zpuaJmp0ydRqTkYjSS^iqNdaWka($8gnvNy7mP~asc6XQLCQvkIMzca(m8Pu_V+G37 z2C#z0z#e0%N!5t#p5k_s`vgw~AbM+rD!3>fT+u%${csSRHFQxjZ%| z7#k@w{j;=MvHk-{nSp~1;2DRc9k%?`DVK%OOPwL8AAG#HjHMk;E9G)wwbvfd8jkVG z5J=;>%Jo+&-`d~*D?4^PapugoO--?E{Y${&o5-Ju!w3uSeBz0-K@MkKuciz65$1cV z4p|!39GiJlmtrf`6d;~j2*PTNafX^7fld6=KmF543XO`|=g%>9{`Y1O9qg&Ee=5+w z&3jk#kzYgEug5d~^vcY5J07z_E$C<*-xhM@aI7I~vZ2Y=j6xocott9ZtL9^ZU4|Db zIobAdkmz4i&@XkN(ovH1tkzkK?WGw8uZ!QNNSui24)6!t+JFW6 zu}RSf4jjPt)#qc-lyQ-u#)*yl6!bIJ8pcnD7#2!QQ5&!`wBYJ;2SZu>FiYdCUzWWf%j1oBbTg9*)#Ez<=e1=2O_jgo;@(tLrLV z4ayu?ax;Sc02DIRj!?b}`uFBqcPUUV`uk$?oAq!s%L&ZU(&uC3tH&lLTD_7P$4ux- zO258qa00|Y3gz$X?IjzIJh(wquPuyb zUs-p(W8;oA=$+wm9$FvM6PpGA(6=1{Did>nL1|9^F8bv)RfR7++9g6jR3LdN;P`y-X8(d9kXf#r$#{YD;Ryi>1(vOh@< z55*#|Egh^)fp{~QX41&Mq>CxWniloXm;k=f>~xuxl+wkC{!&LQDSIYOG~Fc4M{qdy zw|)b|>?2a=nu8t(TOAn}R3xh(f)ddjTGR`h<+ZydlP$w@pXfqoY>GiEJ8 zs!vt$YBpS$_F0Meq#WB8XX4<9HL_(A!H_v%M`XE-e1kCD|Zs zCwD&a#ESm@YHByL`m7GK;!%MO=+T0(eG&YIcC|jLRsx`Zk44W~VU2cKpZ_@YAs|xWBk8_y0}>xVk~+4)KC3XXx35vslmr2U=8G8W(9vL z^wCTe9@j7a;xDqQEYcjfJrRE*?#prlWq3TiSuG%&N+Z=7AOTn=yN{ zX%gon?*NVj+Sf?a@Fz=_{98P=Wya4#1=`%*&70!|lRBmfYiT@Q5!|CH_n@5VL7yxJ zrLDZae;H?G%URCSJxu@;1$0UN*EPnXmhw=7;eg^m5cc0B+jR!OH zc`P)ELFFSZ2JH`-AS?n#zWD~L!1yc%1I7S;p~aX3cgMpi=$AbGLdbs6-98Ud}Y zqzrs_B>I=ff2+`+euG8ugdSS6=ERO2ynrWSy}LV*H^Fy$tv=p#tHF?@1m2tpShH2E_T%vaY><4xLTnM7E`&;U8)=D| zGf|E8nxFC~ofTi%*LP~;MtHD37!~?$*@I)Xk@%UJ+kf*nyLx-MJB)jlID)|p0JPnr zpo(Au$QNdC*BS$ElPANhAWUZCaYMp5QjZ)tf+LmRzw}GLB$?ce=K%J`3xCmE`Tl_T zGy~BeD0+E}mHi)+vj35A0Eqtg#eTid55;HoI6s~t1vaf-d0C!{4?4jH0wgce%!t#y z%%~Bki&Hno0gu;C09EZ1Qq^Tq=IT;MEc%l!d}$JQjb|nMP2UJ_zV5{@estS5{fw8x z%iF7cEbm$fcxAr@fdq*N9-$W7f{t~cKoYMUKzNwTLN)s30+}H8L_z2m1SuZ`}C)mKOBjF9;mGcgd1> zfguyc=jQhO_HXyTg>zU-$){+YRS{QL-266}5_BCJ^jmC>-lU)b@tYryTx0&dFn3LXc0?ZlVlIJnTr71>}G3U%5N}^mFa`2~{ zy!qTAJo$^p#{Ol?-V&4wkH_y1W<(Y$WRp6LfAQi;gFz6Q9Hx7lzcM9Iif{`)!vb!K zO~xpSzmWaiq>&Z^CJUp@A<@U56%CwWMF}A|>>FNk~?F6V@_w)-5 z4u0bL^`F$#v_1XHmm>S)%}q`Bc6I&So;}CM$C>JqTu?TE639&8E?5Rx1l|8+mX?kN)?6 z|M&g(-!Bx9f`+fvULI$Fgjt4`nLngY zV_PFN*n<}Vn;0dktN_oL3@>O7tPPZx;>j1KD)Ll}TV&K@YzvQQ!8Up>X-)gO6ak{n)Dcl~X>#Zq8 z%q=EpQw=e%_VH{+K&#{b*D{H^YY`@ZZKY9ECbn*!Te)(3OywOsnt)nU^!>CwuvO%i z%-WokJvLp${uAzMKDFrQut^9vF?21iAWRhK3GP)yTyrzWQ$!H9EMm-HU`SwDtn)7x$C8+6VhKnXlGsy`sM>>*MOR#6iyO)KN4TsqDN{o?#-2W$^gd}J97 z_}VfJ^d&pN-64JqB;;Kb&8OEOqJ)QQ8PE(_AM~GQc9H0hYNTCkP4NDE_=RvI^rnAT z1^RjGa&959MOC6bS=s>eyXzWrWnyT8@)TRnGchqIUTzwcEBWuGLmVY|>HJTys?N1X&iBO=8#CY(9)^0PZ0Jgk6i0a}mVwvt{AvvPe2} z^ngq5zrT$_RHmMxTG-P;`QARtN;}a<8N_o|9F@59sN)jDcrFqd1_W>d&F(Q$2 zI7Rzy+rFKqU36?G@|pjttD&lDk|!W0CX_-zyv5^N|I1T?$JZ%_p$xuI=0b>Lpl^L_ zmX43vM^zu9EqVABh9<7(3P4MmfXP+J^n~W|(;E}wkBZW9=fjMXP|k*)ZJD4z|MZZ2 zrfKsDJCB`iH_!njlYDxkF3YyC^RQq2e1+R3^zQ=w5lF}F{>)nqP5=!)J<9!2_0rfc zZ9b0#`u7^~5&;zabalLyU^p3DYfJR`yLNr8QrQ@hNUX(~C!Toiwb$Np#~pZlIKIIF zQC~ox(;R-uu};eqdt=!J(Zj@Zz#9BzC6bQzcKyEF6Q7uA0radbzytDr3-O8mRKR^> zT8)WO-*0M^o9wDXKO-jp^G~7AKPCX@PKIFUBpyu2_SB5X3BJ+wjfW#VT41&p%s`(0WqxR}^ zw0aJ(o)bgvUk@CE_pfG=BgkbrTNx4Np#P*mKi4M*`uXkzJ{Ar&0DWPIn(WKv5lRHC zuSom#Fo;*Rh})e7`n{cy;K&BwY@5;ZRz*j{V`500Qm;)B4IL|s zq*pm2y1K~NIyXFAU2Sdhe&!&ax#+h=fc=>{ZBI<*qZ;Qxv297a7Wbj@?Ci!H-KE z7`ug5i@^T6k&$`nzl1egXTG_!#=RK#uYbU%7=!~DMsM+Pe%8WRHm6WZ-a9n3BZ9Pc zCzM~$k;)wOXXQb)avm3{!!xw^NBZg1sD87ep8eO&1NY@yjRvu6-~8q`U;p~o|L8|Q zvhE8QwW*_=ms2wHu4~k z>@9D3i#7J#7YnQWR~YH2V@1O8(+qJH{ZSZwWyJOj^RbBQtT7L{pE>A1T*-$8Pr$n< z+4vqz(&;s!op$3o2+MUB5|R3wQjlS!Q8I}5r~ct#;|Q~WrO~X>Rij_x9s?1Y#_xia z69fI*)r=!%^VsyC7?%N9kwKw#x3-eTS?pME?~=4AWJ9vGHbrF@?G|h^C36Vvamz&- z|8s`W0glxxB_F%7JEx(Y4N^_OW7P<$>1}HxV)=}ag6_5Gwng@$w-3wGuctQ3(fpou zt4Du>%!lR%ArD{r(wE-$wzqK_pv=g^0W|Bwqw&aa0dOf~)u1dP%v*Mw6x>Kh&^%Uu z_&v`)F@|k6IW8auW2ZSQqz#sln7Q}7=RIsht26)@CWjIQ;xAP4VV*@bGw-xOVk!4$ z=s%6QTCb%dLhHRO#4i0^@hp3!Q`h zt=0;*zh8Jv2Q8EmxyIu)%=ANnV-Ey0Jsze#^iYvh;r&-$dH9qm?5l2FCRRMx(vNFQ zUvH5H*E!#f5wJX{yx-=b)9KsQ)kPTHrU=sF>DD1msP)<+AS9oS>{eFcUa^(I{4Xq5 z6K0<}5vbYK;3z=W=api&kUi9{1dobrsVx44@q2O(CsxPN$HPhh_zEEagCnf1VuKki zsz_F7%#cBO8gRN&XqsP!U|#ytmy&vV0J0Ep+w{LKQ!wjT)pIBb4BJ?kj&1{ek$s zMm~yots+^GEI@E%dVnLwPYEV)mt9eXRVoF1Xm-02-P1q`xrnV|zj@WFV|BFUDQJu! zFdRl8z+5VQWTu1gN#}-4BMj~a!3JqXLENjO9CTM=SM5=1W{$Fc6x^fgrG`2FZ5ti; z4M!`cGvG*(9~eJO!R#NL+<;zN4c;ye3TOi#2uqGi*!3A&~ z@D|5~Y=UnYDa4WDscTL>dNck3&I;DKC7rlCMRQyhynit&sgh5`bI!_7ogR8Se zf9CCaF!9_gf-i&q>q2K+WHr((jMO5Q+~Tw#wtrD3+4Q%@Rv^a%Nu7DER!i#s%rv0?O{a~#SU?2~G%VVk^8%D~?@W@bwX*34OVroP9VI7RA zhAObMviO)ESb4t1#~?EZjCdj5ZHWf@KtB=MK+xz0D+*`m*Wx)TLw_|xe`)dT%`BeU zd9Dxi&nJ{9>hj`3zC_LyN;VSV)}r`zQBW7HWd6D2HaY?9K$ywVA924!qCbK+n9qqI z>McRUm2pynZQ;n)%8J-UHV;03Zo27Uihe^9ux=FP|6zy9zM!AIBo3Y}!RlvC6rce5 zOC5lnmY=~aiK8%H+#-9*ktRr3f~K(Go{5o`i4F&OH`c}!ytzl{*U`YZ4}kuTcf8|y z&wC#BUp-%(21+$(HJ}+o5OZ6smc79Z`ru>sQPv?Sf8kp;>f@7o1{s_-yz*1h36z5h zfhCG_pd}h8qP`{2JTK}gElud3mEK*INj~fJcEuv^M)IkX`!%Xw3nc{lZ;rkDcX+`D z)0uF(DTKA!pNeAj@bK2G0A5}sZ*;h-$?5m)wUCRtU^Gl0Q3CefkwS1}`t#T%IE`_@ z$l-Rjf4u(s>uvbO`qKaQwjDe8Z+ySMA0dbRwl0Y59JZ&02XM2gAVV#gckPL*4xs7K zIm9!Uj`I;Gcy-V@^NZ=KZBGZas3^WSg@=tQ$6IT)Wt`3d_lC}|aU00x01-Pq(NJhR zfEdEYD1*^mkgBet%!eEn&ZaKG%nW{WtOh`+)Z;7<&=dl5LAE9m z>>u-fTfrC{ot;KO+ZJ19hO|STfIUE^A5VWh09H@2 za5Qta6OqWT%chVA0&On&WTVocLi{}@?duJ-s+Ll( zbE9HxM{Jv*sFu|`W_u`mNx=_n-pq0FvPl3JO!uu>)0s%Wdi2q3HUz){4T}QG>?ITU z8C(r!X3|sHT7t(`3ex(?oc;EQV0G;4COt0({g#C*idCMZ7T}I(-lcIs*TQqsR-OO{ zpZ(|Oz(+s&(f7Xhy*xV)hybJ8i$^0u)?!#F0|Yf#D9+~i9H#`v6=~=nWYO$1z*>Z;4#{U%Vm)( zF$xVF+<}w`3W;5_-W&WI=B;#Rl!p|~MG>r!)+^~S~ z9ex4FGuQ1W0^(UZ3vP2?tWwZVJq=iK3>W)rqKfSj&~FVNvl%mQS7kK0f3wMxkzl=v zeTgn4D-!zW7sXO+wF$_pqgd^5U_aM%8@x6m09E*H3Cbr~|3m+OnF*Dlf3uAfqcXxY z{-a}~0oD{|V+?cI+Kp{%hyA3ADKjp-a9=7K&X&XJt&d4`kWm13)#i(!A4vepF<;gS zS^Szm&PBdX44sMC%YyfhM3@R;mNXKC-^@=WG3qGtaE5y5A7nBBzEnR)E=L3sxTVR< z2E+QxrF?6mKqt&i8RcC*R+bMlah~PE=EI*Hq<7wVr|i6J$8UpiePrC4bV@js?$w+O zDt!02|D}nw=rV&Tte7hWUY-DAGa#3*R{NtX5>BPk9a8PO$a{NQjFq_v|~ThIXg zjK*5;!9N7_57-b1lq0d&NBQ>KZ`b+twXb~*VTrIg0(-26t!)jkT4|KICLoRxcm!AY zJd1`C1`*VlQ;Uu2kg?Zbkp`=WIu02?1G25Mz}6{@n{VFtyWdg5S7W@s-l@U+*Q@o0 zS$P0gaTrv3;r(1eovqdWNR)0-EKb~gD2XI1tS8Bx0?G|>ACp#ffYO^i67dlyCX45A zLVst*`zJ>V^H$47ShvRznlWUAVef;-aOUFrK?!EJxOgt@Fci#akdcafuzyO53DxKP8%-D_OHuI&accoUu)8G?`@)}QJu;EkPc2c8(~-n zg@-kuAJ;GZp65mE!Ay*N$WpMdnn#gdc;zdslD}i91quKw&s&N;D9wQ0mJ2prhH*HD zFj!CS3mNchTvQ2WVKGH;Wc>T0i_TxYdK$`DTFj|sq=US_jB-P+uGPA0wYyV{Eqz{p zmrN<^$y0?;Nqee8d8_mKi%k&#blIJdF&D(!COYH&JJ(NV0u4+2Os-?{^Z0=NY9 zY=+ywpU6L8{rG$C5p5GYI_Lwe!(SJ@_Gvu^#yc4AsXN&mu@e3IfUT}YpxhNaQK0{{ zXsW!9y{XlsFL0kRD!q!l)qko*bOweu1p4y+CR$4xdT4b!95n)r;-avYK@ zHJsj38XOv2wBn)%1vCJE0rak|dDUkQ zMHEH9t--W}+Kqj7d;75wqIhc7)o?rPA2&0L)gl2Ldi0kxfS;H!^6z}-I~anj4g_%F z@#?881#E>?wXpybqT=*_-H`vzpuXD~heQ0R*_U$8}KP~k>{5*-rKa50Ft92n@(*zq+Y!Y=^&+mSYo_3n73I$PHlFJ4LA*ncK?0| z;5A6G0|VrSnA_zs4f|pLba0x|!9*JfOsi}UteXt}2m0-;jUQk*%FK6!$d!(tRJ4)npL&?f{4iO6<(T8-fM2trzrT_{#n*26#wg^E}@ zev=Q_og1ezKgbtR9@ryD1wG`qzx^#2k-OM0_JZHxE!>y8=goWg&wt+9+Y8``NclO{ zoQOV3_*=%BW2Ck|KpnoD=k(3vzBK}{AAwnsMnsB!V+3p`VYG}%^i#4l;!`2*juxRpBg&s zu*DZ%ICF4t?)L4!7#Sf^Rl#PAVtxIUhSyss0LQk(9oxh9KUpa`&>xKx1O1UqGcyWg z_oolHG}tiK&g!-nI63#;dynexw~=x;0@&8A8|Tl5x53hcyG_+$QOH7&jRoPjsJTp>C}1z0+1WE(9?j)<7Get-}~P8Ui;eD5-VY{yfUG_EhwKj za|na$9FOQo09PZXHhxY>*f~H)UA8N$c6}u!msH)1iS@Vjh!Hf3+KqpjI9y{8Ez8bV z_{;>vp?UM>|K>MqdwWMlvq&B?(C1@9^L36+0{zwljpFOa7$26kjAqy}dS;|4?6Xeb z0hvJ&foy|ER@?30|Nabwne^koh9)~Y&cEamMO7F-EXQCKc9%n$Eo6Jjuq_ND2)k^m zyshN@#@UllcurRrB(fIXpC0fF=J-b}*AsDYDsp~4``OQC9k6C9l2888B=CQM? z%{DC;V6ub>G*nsTWBSpclWH1pbeRaM6rZr?E9v-I@z zMI2FGY-VeqZ}zRRp8&#oDOOwgeV~7e_McMj7yWjbhL1)v4aC#iqDhn`#%T{{vK+`| zzyJMlg!DwOBpCLF_I5ZrdaVIlJUn{|B{1P|M=IqPu>8P~*d9JmZX4Ul26NF&LVn>W z#I4}{5Ww%FgDn4^&;#2j(g?|1Al|%qpa~@)%qYN=t!4OtMg_jLdi845VwS}Q<181u zfGG(UV}O=kGbK+uG;40+1N;I92qeJ?fF44vKK2L}1NyDx&XK~mxT(3CqdKRYA4JgS zqxqU{c5AdWwCJA}#j_^V6;hV>gZ>jNni3ywey~P_LddPBEn7DCPk*8}kw5;| zeGLmYc?gDWE;Z2Pps=ATh9kH>B2y&|Yl)&z-E`AU7E!d@tnugS158+OzM^EsyINOa zJP|aolo}(J-{?s?OK7+bajETAO=p`UjqUluhNJTi)+|ujBvxt zFTY%tA=+T)*p76oUFYU=gC81;pcF3xV0h&0!gfoc%XIcB_aV ze;t5POEhq5XtzD0KWGE=&(0;E2-@Bnp+v_f6{6NO`ft7EmgyTeUiFP{klrHt?Mh~E zob{Jpx@_}iJw9v$fHpahWknEz*&qNI;mA+1`qmFrpwiRHb$7JAQn{eJn@|2R1=-f> zdKo>@2a7}MFjxeaJg@2PMz*0EflC;(ydU-qaJk4=I3KVMSyYSbW{}ELc*|@u;I1Kv z0s1*upevTwfD0zbTF-dKGfc8mTTmc5` z0$Dl?Ah&QV$QMX^*R9SRF_Po~#2YQ1>cbyQagy*Ybm9+x z_`{sQIr<%C6kU_JKcpu>oB!&x;iFTeLucp!`=Ws3BZ)jtjH0<)B8h$|l4+(zgZ2MA z<_-_PZ__4B_dtKXEo#(2qB={@%{LRJZpfzvC&J{toaF+>Cvk5FK&2USDGg_UQ^1&QouthkUXG!9!Is z2~K2rC8a6#C>It+!$#Q3+q8P5BQy1+D2RaqO(Yu>F>*@C%|VBLv^DW|Bz$DOzi$g- zcsQh;fyCqIKpP4Dy=h)Gg&<2E-jMzcH@qXib{vEhcN_^aJv_ZTDNP8%!WLagPDHk@ zKtM`H9_*m-B{EyF>fufstW+kGA4rGYp&=yP*+qfAR!PDbroLH~3WD4Z$QvSIK*ZM- zF6a7_X#j3y5@0u&hS5`{^RcX)r;WJGbu9a92v&V8W!NIJ{1kiz3C{S=Nin7Z2jRMX zLql*bKKj6f2@94iAxNpQ(erUl0C~teaC|;|U1=vne`oakcrYkof0j6J3c<|(w09Q1 zjWkIVpLTYWI7Eifmkr}FGxG_KnNdXd_Z%}bGc&^nVPLlDigw%`Z^x*_FDT2_3(;v=Yp+ZCA~pS@1`JF zf|J@mXW|m*ujjxCijzXZU;fD|e0!?@?k((K7sp@z!uXvjMgJ>BP>Q&WF`LFzQ%E_Y zLdqPJ0sSzcLrZ_=8SU+}#8)*|vEX()Y3Xd()V{NF-{^ zk?-3ADy7C88#7}}lRYG94^J9XV@#1Ti!@?cH|7xjWI_MJZ-TM!7Wz}#pJ&vexP#{lv9Z{f-nqqg8d^!s4C8P~{I&qtxcj8?d= zpnIz3kYo7y_lD5##OHsZUoHUXr{kT3jh$OYvAFnTuB-vM52co&Db9IZ;-%&MST zMCF6nN<^1=z`mF|ua&*e*#2o1BY04r{W-1MZ>+S56UZw7;kq%e_DVFOO5cE)-7MMp`PK)dC#t;mLZtS(?~4#^nLca;AthiPTGdfkWQd z8))U-qIRw`pg(3?#>(~=@%2tI{0|H919w$^>Y5j=_3EG_~w_N?{ujq z(SvHFMaGjB5;{By9EpI1{>(Pgdi6YO%u4=9^0(01$I?)Ql;9h0C!)k_QiyL*d-rLz zU>KF{&$_~Jew^%K%WUuDA*u@SH@pXCfCAC5&WqLH{~dv_R+WSp2jaEBYZ4`)iWgCB zpjhDV^?22n4T^vLOZBfpu09aoDu-$cvL*PG^E&Vf{VJWIh6)<~)Bo>amy-&d;bL}+ z{g%lexu<4;T=zh&ZPgZzwxOV38|zQdK|j#M_BSgrZxkLphj9~RSHYqqX&&3jlX)z* zUx-gA8Ar=k)*|ww|1<>jJE3L*fBfGI{r46D3OhoyGOtllonec9KAJ#DDh7!1k#*Kj z)OBWwN#hMc4&>pU3^u*`)R}Y?J%K*P>l&p;`RJGUW7e@ejXf|xg%Qrt_8$# z9T6VK7&oC$>zKR&mpke_u+CI407;Gt9UBnqaPgn3{h`A!twHE7v*&V{AHE-iyL5;C z5XJESeuRFX5g;dQJqCYfYRt{P_gWX zVOl+d!%Q`;@`Ap_!}C@ucjym6^oPBV2p}=%{dfR^p9-=GL-Bl+hv@*C*7oW4Mi?d3oE38i4*}cv7#V>WKAxM7DMEaYq-oZFCZzI06_pC-3PDWHSiE@0aZl zcs^@fJvv4_%eJCov8WYH=L^BFk~?Zw!-Y}Me@&}kM^Na`4*gETDT{J(t7l~@`S&k) zKCfhOmfKnxx9IOu{%1{Vha2o@=tJKH&5u<_0&+O#ym}*0i)GA)8CqyN)<#_Jbl#4W z(ANPZvZL7-ko^M{Ht~EoBq_hY7i6z;Wzau!23gzV8>`=M`>x{Nyqz@jb-{+=6+eYS zL)_4$Wb2`AxQTKV#SeX-j}u3L!c8JhG2~yv3|r464XE(uLF8T7A1o>_81H`VEP;%0y#wz4zsSo+WE=uD9+ zMdRz!ff&!H1ZzDBe50U$1iebtYTMZ~`aysm$({#xIotIgUe_t~r_^dZaSLiVJ8=Za z%K=pp{Ufkm!`}D9giWH=9?-vyb?=bvA4adVW!bgY-v{d-AoZCwXYrgLl5Vn2(K-Y( zypEPqPMq9S0u=5Tr;d7G!W(&%^8Hi5)JDA%FrgfmX*7WTLC|00c?P|KkKRG!*l6~4 z(Fp?L8Ek*QmX@c8AFdR1+nhLwga-YB9QcPE6;`{*^Wf79{dhj)i>k2P!x;5Cp+BjN z=K>{(-lgrInZmu_qiAlQU0wkFkB3NQw*d4`90BriAh`ANU+kJ5X05lB#j>$L)H`O; z51L1Hl*|wf$H|9gHdME=()?^0AVCX z@nL8ZXy*a+!HnM@K>rEouhRjV5$*9b%J+AwBX>D&)s8r@3|F|V-P$!N6 zg)6BbI`wiD0{Ec6Q~maW=pPpPjdnV;qE9hPbn?9y$tFDupxec@Q*FNZ9dm{K^*=zE zTZ?`tjsS(W53w+e^E05;u0hZr6Z&K7!7;`}aV@ac+o^+;DMh_|Y`l9kH|%Rm(j$Zf z!;s|kmJ+~;BLHFwj?Y1WCz7dr(AIabe!uGZc<51~e-iY!;q8nw6*R()O`_h%bK9d? zYDfw3hc^rTPTX$L8}A>=kHY##;K1WlJnLbd?eh7wG=TmimWQ|5YWi=8`wx7le)9d1 zvyWiKhxo;>G8;H?a-o=8^8)>wUGZEE_TCTZ$FdQh)(OdV9G+f9!+*x(-?1J)0l-^% z&LcQfQGVsOM8m(~w%qfOo)jleK;~`bKm?#a5BKJp4$g0=p4=)|Z6L|BkFh@yj*B=O z*~K(c2KxVPx0f;rd{V#SpLaQNQgkQqEUoSVQEdDQ_y3z~hEJEdRBub14W&Pr=2(g+;$Ggs9y~I!ix`+I%O6E z{kz8ej?lS(Oa;v;i>k4K0)X|lUBvb zd!YYJR&;!(ED zZY33aNH#XpH@hhJ>p5`*xb;hM{$-L&M<93-d;fT>`!rrqJ2xz2`@4kxIg+G?n`gMc z)AO2`3KGT~qntSOI|AGxE7E?!-#QI~l@;YJdn&EdM$%ePYr#Rf zf{#?8t)@=gUZA^1lAI*=u>D@p_JeZSek#&&PmxY1H>xZB6kAp+&=;)bP;IYcIbF_8 zas;@en>oK6bDpw(zcx{iiU|jx-|CsO`Nbf9njBF%`aG1dPSGlV!D{vEK7bf|+RO(r zZajW4j!}*P;YyU_2T|GkG$`)}wfjPU5vU!4DkbXr5tHqowP%sFHPt{2pQ!qX+dN3M z%ba6SQfivEgWpx99ZegIn=_s9I$W$G#^Vqy!rs1R&xs>Is6o$DSR&Y0HPS2SgXD#- z3jHmZd5iwK0Q$>h@0SATPhtDxJbw$ICly+x(JiS~Sb@I6qvq`N!lP!nCyKtn^DB7l z62G{{cW`pk@Ni!Vz?uh_2dYx4YNXhE(BBIBSyaaM>kMa^5Wft*>uf3sa3^H@_h5pt z=x4l0l_8U${}_}v@M??xdS0DDe>wUxgSD!cvF+R;Hv@zzEC4?rkd0ZyYfE;Em@p~Z zkHNjaF!ctWTYdY)|cng zkg@&fCWC$j)*G350J~nNd0+`x7wfTJ7Sdoo>*?vs>vCvavt8=QEe_e1PwvDwd`|XA|SQ zpuADp&o%DPWItoF{mTLLE9&L33|7jyGC|uP46H@Ab!MvFFjGU008X&Mk5T@yPKg>L zZ)XMD-zX!!sm?SzAiG)UUje4Hc?JEl{X##U4+N_SM7>o)|Mi}aUa_skwS5Rs%kGTj zkW)jB08TL8&%_=t@?$RgHz7c?+>#{%Xao9}wAyuu9tYe+y|Vp0s+`M1FAky~WP1Vh z8=*gwR$7N(5%pNuHin%tXxkR3T2=XyW}cv)@0k8qY+ zw0Uk3$VTPs*JTh7+$+^DISS$p=r`OJ5&8)su>Bz32>p$WAIj0X$`jO3$4S^TfRiYG zOP7+QDG+bKBR&6OfR5cuSf^HomG+W zC^OY)dP%5!{46hW5^v?jzsDkOrZ1mRz&>v?!i2f}u zp1wEf2 t`V-psw#oRB(n{rOtVV6qFtLICENSE.md https://github.com/Reloaded-Project/Reloaded.Messaging https://github.com/Reloaded-Project/Reloaded.Messaging - https://avatars1.githubusercontent.com/u/45473408 true + NuGet-Icon.png @@ -17,6 +17,10 @@ This package exists to allow you to use various features of the library, such as True + + True + \ + diff --git a/Source/Reloaded.Messaging.Serializer.MessagePack/Reloaded.Messaging.Serializer.MessagePack.csproj b/Source/Reloaded.Messaging.Serializer.MessagePack/Reloaded.Messaging.Serializer.MessagePack.csproj index 9ec24ad..0a3f805 100644 --- a/Source/Reloaded.Messaging.Serializer.MessagePack/Reloaded.Messaging.Serializer.MessagePack.csproj +++ b/Source/Reloaded.Messaging.Serializer.MessagePack/Reloaded.Messaging.Serializer.MessagePack.csproj @@ -9,12 +9,13 @@ Sewer56 LICENSE.md https://github.com/Reloaded-Project/Reloaded.Messaging - https://avatars1.githubusercontent.com/u/45473408 https://github.com/Reloaded-Project/Reloaded.Messaging true 1.1.0 true + + NuGet-Icon.png @@ -34,6 +35,10 @@ True + + True + \ + diff --git a/Source/Reloaded.Messaging.Serializer.NewtonsoftJson/Reloaded.Messaging.Serializer.NewtonsoftJson.csproj b/Source/Reloaded.Messaging.Serializer.NewtonsoftJson/Reloaded.Messaging.Serializer.NewtonsoftJson.csproj index eb9ee73..b9c854b 100644 --- a/Source/Reloaded.Messaging.Serializer.NewtonsoftJson/Reloaded.Messaging.Serializer.NewtonsoftJson.csproj +++ b/Source/Reloaded.Messaging.Serializer.NewtonsoftJson/Reloaded.Messaging.Serializer.NewtonsoftJson.csproj @@ -9,11 +9,11 @@ Sewer56 LICENSE.md https://github.com/Reloaded-Project/Reloaded.Messaging - https://avatars1.githubusercontent.com/u/45473408 https://github.com/Reloaded-Project/Reloaded.Messaging true true 1.0.1 + NuGet-Icon.png @@ -29,6 +29,10 @@ True + + True + \ + diff --git a/Source/Reloaded.Messaging.Serializer.ReloadedMemory/Reloaded.Messaging.Serializer.ReloadedMemory.csproj b/Source/Reloaded.Messaging.Serializer.ReloadedMemory/Reloaded.Messaging.Serializer.ReloadedMemory.csproj index 0a1c0ef..2563dc5 100644 --- a/Source/Reloaded.Messaging.Serializer.ReloadedMemory/Reloaded.Messaging.Serializer.ReloadedMemory.csproj +++ b/Source/Reloaded.Messaging.Serializer.ReloadedMemory/Reloaded.Messaging.Serializer.ReloadedMemory.csproj @@ -7,12 +7,13 @@ Basic Reloaded.Memory based serialization implementation for Reloaded.Messaging that converts structs to their raw byte representation and back. LICENSE.md https://github.com/Reloaded-Project/Reloaded.Messaging - https://avatars1.githubusercontent.com/u/45473408 https://github.com/Reloaded-Project/Reloaded.Messaging true 1.1.0 true + + NuGet-Icon.png @@ -28,6 +29,10 @@ + + True + \ + True diff --git a/Source/Reloaded.Messaging.Serializer.SystemTextJson/Reloaded.Messaging.Serializer.SystemTextJson.csproj b/Source/Reloaded.Messaging.Serializer.SystemTextJson/Reloaded.Messaging.Serializer.SystemTextJson.csproj index d365a5c..1fec9d9 100644 --- a/Source/Reloaded.Messaging.Serializer.SystemTextJson/Reloaded.Messaging.Serializer.SystemTextJson.csproj +++ b/Source/Reloaded.Messaging.Serializer.SystemTextJson/Reloaded.Messaging.Serializer.SystemTextJson.csproj @@ -9,11 +9,11 @@ Sewer56 LICENSE.md https://github.com/Reloaded-Project/Reloaded.Messaging - https://avatars1.githubusercontent.com/u/45473408 https://github.com/Reloaded-Project/Reloaded.Messaging true true 1.0.2 + NuGet-Icon.png @@ -25,6 +25,10 @@ + + True + \ + True diff --git a/Source/Reloaded.Messaging/Reloaded.Messaging.csproj b/Source/Reloaded.Messaging/Reloaded.Messaging.csproj index 9d8bc13..d79a67b 100644 --- a/Source/Reloaded.Messaging/Reloaded.Messaging.csproj +++ b/Source/Reloaded.Messaging/Reloaded.Messaging.csproj @@ -19,11 +19,12 @@ $(DefineConstants);USE_NATIVE_UNSAFE 1701;1702;NU5104 true + NuGet-Icon.png - + @@ -32,6 +33,10 @@ True + + True + \ + From aca114cc9271e6c481338c694f64c654b195aaa8 Mon Sep 17 00:00:00 2001 From: Sewer 56 Date: Wed, 6 Jul 2022 00:11:47 +0100 Subject: [PATCH 06/16] Added: C#10 File Level Namespaces, Some Docs --- ...aded.Messaging.Compressor.ZStandard.csproj | 10 +- .../ZStandardCompressor.cs | 91 +++++---- .../ICompressor.cs | 31 ++-- .../Reloaded.Messaging.Interfaces/IMessage.cs | 19 +- .../ISerializer.cs | 35 ++-- .../Message/ISerializable.cs | 25 ++- .../Reloaded.Messaging.Interfaces.csproj | 1 + .../Serializable.cs | 91 +++++---- .../MsgPackSerializer.cs | 72 +++---- ...ed.Messaging.Serializer.MessagePack.csproj | 10 +- .../NewtonsoftJsonSerializer.cs | 53 +++--- ...Messaging.Serializer.NewtonsoftJson.csproj | 1 + ...Messaging.Serializer.ReloadedMemory.csproj | 11 +- .../ReloadedMemorySerializer.cs | 65 ++++--- ...Messaging.Serializer.SystemTextJson.csproj | 1 + .../SystemTextJsonSerializer.cs | 53 +++--- Source/Reloaded.Messaging/MessageHandler.cs | 107 ++++++----- .../Messages/IMessageExtensions.cs | 25 ++- Source/Reloaded.Messaging/Messages/Message.cs | 175 +++++++++--------- .../Messages/MessageBase.cs | 29 ++- Source/Reloaded.Messaging/Overrides.cs | 29 ++- .../Reloaded.Messaging.csproj | 1 + Source/Reloaded.Messaging/SimpleHost.cs | 129 +++++++------ .../Reloaded.Messaging/Structs/NetMessage.cs | 65 ++++--- .../Structs/RawNetMessage.cs | 69 ++++--- 25 files changed, 588 insertions(+), 610 deletions(-) diff --git a/Source/Reloaded.Messaging.Compressor.ZStandard/Reloaded.Messaging.Compressor.ZStandard.csproj b/Source/Reloaded.Messaging.Compressor.ZStandard/Reloaded.Messaging.Compressor.ZStandard.csproj index 50b4a98..6f8270b 100644 --- a/Source/Reloaded.Messaging.Compressor.ZStandard/Reloaded.Messaging.Compressor.ZStandard.csproj +++ b/Source/Reloaded.Messaging.Compressor.ZStandard/Reloaded.Messaging.Compressor.ZStandard.csproj @@ -2,6 +2,7 @@ netstandard2.1;netstandard2.0 + preview Sewer56 LICENSE.md Basic ZStandard compression implementation for Reloaded.Messaging based off of ZstdNet. @@ -11,18 +12,9 @@ https://github.com/Reloaded-Project/Reloaded.Messaging true 1.1.0 - true - - obj\Reloaded.Messaging.Compressor.ZStandard.xml - - - - obj\Reloaded.Messaging.Compressor.ZStandard.xml - - diff --git a/Source/Reloaded.Messaging.Compressor.ZStandard/ZStandardCompressor.cs b/Source/Reloaded.Messaging.Compressor.ZStandard/ZStandardCompressor.cs index 3537063..1ec904f 100644 --- a/Source/Reloaded.Messaging.Compressor.ZStandard/ZStandardCompressor.cs +++ b/Source/Reloaded.Messaging.Compressor.ZStandard/ZStandardCompressor.cs @@ -2,45 +2,58 @@ using Reloaded.Messaging.Interfaces; using ZstdNet; -namespace Reloaded.Messaging.Compressor.ZStandard +namespace Reloaded.Messaging.Compressor.ZStandard; + +/// +/// Creates a new compressor using ZStd. +/// +public class ZStandardCompressor : ICompressor, IDisposable { - public class ZStandardCompressor : ICompressor, IDisposable + /// + /// Instance of the compressor. + /// + public readonly ZstdNet.Compressor Compressor; + + /// + /// Instance of the decompressor. + /// + public readonly Decompressor Decompressor; + + /// + /// Creates a new compressor based off of ZStandard. + /// + /// Sets the options used for compression. + /// Sets the options used for decompression. + public ZStandardCompressor(CompressionOptions compressionOptions = null, DecompressionOptions decompressionOptions = null) + { + Compressor = compressionOptions != null ? new ZstdNet.Compressor(compressionOptions) : new ZstdNet.Compressor(); + Decompressor = decompressionOptions != null ? new Decompressor(decompressionOptions) : new Decompressor(); + } + + /// + ~ZStandardCompressor() + { + Dispose(); + } + + /// + public void Dispose() + { + Compressor?.Dispose(); + Decompressor?.Dispose(); + GC.SuppressFinalize(this); + } + + + /// + public byte[] Compress(byte[] data) + { + return Compressor.Wrap(data); + } + + /// + public byte[] Decompress(byte[] data) { - public readonly ZstdNet.Compressor Compressor; - public readonly Decompressor Decompressor; - - public ZStandardCompressor(CompressionOptions compressionOptions = null, DecompressionOptions decompressionOptions = null) - { - Compressor = compressionOptions != null ? new ZstdNet.Compressor(compressionOptions) - : new ZstdNet.Compressor(); - - Decompressor = decompressionOptions != null ? new Decompressor(decompressionOptions) - : new Decompressor(); - } - - ~ZStandardCompressor() - { - Dispose(); - } - - public void Dispose() - { - Compressor?.Dispose(); - Decompressor?.Dispose(); - GC.SuppressFinalize(this); - } - - - /// - public byte[] Compress(byte[] data) - { - return Compressor.Wrap(data); - } - - /// - public byte[] Decompress(byte[] data) - { - return Decompressor.Unwrap(data); - } + return Decompressor.Unwrap(data); } -} +} \ No newline at end of file diff --git a/Source/Reloaded.Messaging.Interfaces/ICompressor.cs b/Source/Reloaded.Messaging.Interfaces/ICompressor.cs index 33afa45..da76b77 100644 --- a/Source/Reloaded.Messaging.Interfaces/ICompressor.cs +++ b/Source/Reloaded.Messaging.Interfaces/ICompressor.cs @@ -1,20 +1,19 @@ -namespace Reloaded.Messaging.Interfaces +namespace Reloaded.Messaging.Interfaces; + +/// +/// Defines the minimal interface necessary to bootstrap a 3rd party compressor. +/// +public interface ICompressor { /// - /// Defines the minimal interface necessary to bootstrap a 3rd party compressor. + /// Compresses the provided byte array. /// - public interface ICompressor - { - /// - /// Compresses the provided byte array. - /// - /// The data to compress. - byte[] Compress(byte[] data); + /// The data to compress. + byte[] Compress(byte[] data); - /// - /// Decompresses the provided byte array. - /// - /// The data to decompress. - byte[] Decompress(byte[] data); - } -} + /// + /// Decompresses the provided byte array. + /// + /// The data to decompress. + byte[] Decompress(byte[] data); +} \ No newline at end of file diff --git a/Source/Reloaded.Messaging.Interfaces/IMessage.cs b/Source/Reloaded.Messaging.Interfaces/IMessage.cs index 9d40916..b644010 100644 --- a/Source/Reloaded.Messaging.Interfaces/IMessage.cs +++ b/Source/Reloaded.Messaging.Interfaces/IMessage.cs @@ -1,15 +1,14 @@ using Reloaded.Messaging.Interfaces.Message; -namespace Reloaded.Messaging.Interfaces +namespace Reloaded.Messaging.Interfaces; + +/// +/// Common interface shared by individual messages. +/// +public interface IMessage : ISerializable where TMessageType : unmanaged { /// - /// Common interface shared by individual messages. + /// Returns the unique message type/id for this message. /// - public interface IMessage : ISerializable where TMessageType : unmanaged - { - /// - /// Returns the unique message type/id for this message. - /// - TMessageType GetMessageType(); - } -} + TMessageType GetMessageType(); +} \ No newline at end of file diff --git a/Source/Reloaded.Messaging.Interfaces/ISerializer.cs b/Source/Reloaded.Messaging.Interfaces/ISerializer.cs index 06e96fd..89b8c97 100644 --- a/Source/Reloaded.Messaging.Interfaces/ISerializer.cs +++ b/Source/Reloaded.Messaging.Interfaces/ISerializer.cs @@ -1,22 +1,21 @@ -namespace Reloaded.Messaging.Interfaces +namespace Reloaded.Messaging.Interfaces; + +/// +/// Defines the minimal interface necessary to bootstrap a 3rd party serializer. +/// +public interface ISerializer { /// - /// Defines the minimal interface necessary to bootstrap a 3rd party serializer. + /// Deserializes the provided byte array into a concrete type. /// - public interface ISerializer - { - /// - /// Deserializes the provided byte array into a concrete type. - /// - /// The type of the structure to deserialize. - /// The data to deserialize. - TStruct Deserialize(byte[] serialized); + /// The type of the structure to deserialize. + /// The data to deserialize. + TStruct Deserialize(byte[] serialized); - /// - /// Serializes the provided item into a byte array. - /// - /// The item to serialize to bytes. - /// Serialized item. - byte[] Serialize(ref TStruct item); - } -} + /// + /// Serializes the provided item into a byte array. + /// + /// The item to serialize to bytes. + /// Serialized item. + byte[] Serialize(ref TStruct item); +} \ No newline at end of file diff --git a/Source/Reloaded.Messaging.Interfaces/Message/ISerializable.cs b/Source/Reloaded.Messaging.Interfaces/Message/ISerializable.cs index 1d00d00..5d76aa5 100644 --- a/Source/Reloaded.Messaging.Interfaces/Message/ISerializable.cs +++ b/Source/Reloaded.Messaging.Interfaces/Message/ISerializable.cs @@ -1,18 +1,17 @@ -namespace Reloaded.Messaging.Interfaces.Message +namespace Reloaded.Messaging.Interfaces.Message; + +/// +/// An interface that provides serialization/deserialization and compression/decompression support for. +/// +public interface ISerializable { /// - /// An interface that provides serialization/deserialization and compression/decompression support for. + /// Returns the serializer for this specific type. /// - public interface ISerializable - { - /// - /// Returns the serializer for this specific type. - /// - ISerializer GetSerializer(); + ISerializer GetSerializer(); - /// - /// Returns the compressor for this specific type. - /// - ICompressor GetCompressor(); - } + /// + /// Returns the compressor for this specific type. + /// + ICompressor GetCompressor(); } \ No newline at end of file diff --git a/Source/Reloaded.Messaging.Interfaces/Reloaded.Messaging.Interfaces.csproj b/Source/Reloaded.Messaging.Interfaces/Reloaded.Messaging.Interfaces.csproj index 84d9b85..93d15ae 100644 --- a/Source/Reloaded.Messaging.Interfaces/Reloaded.Messaging.Interfaces.csproj +++ b/Source/Reloaded.Messaging.Interfaces/Reloaded.Messaging.Interfaces.csproj @@ -2,6 +2,7 @@ netstandard2.0 + preview Contains all of the interfaces (and some extension functionality) used by the base Reloaded.Messaging library. This package exists to allow you to use various features of the library, such as serializers without the need to import the dependencies of the base package. true diff --git a/Source/Reloaded.Messaging.Interfaces/Serializable.cs b/Source/Reloaded.Messaging.Interfaces/Serializable.cs index 2a40213..e21a21a 100644 --- a/Source/Reloaded.Messaging.Interfaces/Serializable.cs +++ b/Source/Reloaded.Messaging.Interfaces/Serializable.cs @@ -1,54 +1,53 @@ using System.Runtime.CompilerServices; using Reloaded.Messaging.Interfaces.Message; -namespace Reloaded.Messaging.Interfaces +namespace Reloaded.Messaging.Interfaces; + +/// +/// An extension class providing serialization support to implementers of . +/// +public static class Serializable { /// - /// An extension class providing serialization support to implementers of . + /// Serializes and compresses the current instance of the class or struct + /// using the serializer and compressor defined by the . + /// + public static byte[] Serialize(this TSerializable serializable) where TSerializable : ISerializable + { + var serializer = serializable.GetSerializer(); + var compressor = serializable.GetCompressor(); + + byte[] serialized = serializer.Serialize(ref serializable); + if (compressor != null) + return compressor.Compress(serialized); + + return serialized; + } + + /// + /// Decompresses and deserializes the current instance of the class or struct using the + /// serializer and compressor defined by the . + /// + public static ISerializable Deserialize(this TType serializable, byte[] bytes) where TType : ISerializable + { + var compressor = serializable.GetCompressor(); + var serializer = serializable.GetSerializer(); + + byte[] decompressed = bytes; + if (compressor != null) + decompressed = compressor.Decompress(bytes); + + return serializer.Deserialize(decompressed); + } + + /// + /// Decompresses and deserializes the current instance of the class or struct using the + /// serializer and compressor defined by the . /// - public static class Serializable + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ISerializable Deserialize(byte[] bytes) where TType : ISerializable, new() { - /// - /// Serializes and compresses the current instance of the class or struct - /// using the serializer and compressor defined by the . - /// - public static byte[] Serialize(this TSerializable serializable) where TSerializable : ISerializable - { - var serializer = serializable.GetSerializer(); - var compressor = serializable.GetCompressor(); - - byte[] serialized = serializer.Serialize(ref serializable); - if (compressor != null) - return compressor.Compress(serialized); - - return serialized; - } - - /// - /// Decompresses and deserializes the current instance of the class or struct using the - /// serializer and compressor defined by the . - /// - public static ISerializable Deserialize(this TType serializable, byte[] bytes) where TType : ISerializable - { - var compressor = serializable.GetCompressor(); - var serializer = serializable.GetSerializer(); - - byte[] decompressed = bytes; - if (compressor != null) - decompressed = compressor.Decompress(bytes); - - return serializer.Deserialize(decompressed); - } - - /// - /// Decompresses and deserializes the current instance of the class or struct using the - /// serializer and compressor defined by the . - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static ISerializable Deserialize(byte[] bytes) where TType : ISerializable, new() - { - var serializable = new TType(); - return Deserialize(serializable, bytes); - } + var serializable = new TType(); + return Deserialize(serializable, bytes); } -} +} \ No newline at end of file diff --git a/Source/Reloaded.Messaging.Serializer.MessagePack/MsgPackSerializer.cs b/Source/Reloaded.Messaging.Serializer.MessagePack/MsgPackSerializer.cs index 88e651b..19d9dfa 100644 --- a/Source/Reloaded.Messaging.Serializer.MessagePack/MsgPackSerializer.cs +++ b/Source/Reloaded.Messaging.Serializer.MessagePack/MsgPackSerializer.cs @@ -2,45 +2,47 @@ using MessagePack.Resolvers; using Reloaded.Messaging.Interfaces; -namespace Reloaded.Messaging.Serializer.MessagePack +namespace Reloaded.Messaging.Serializer.MessagePack; + +/// +/// Serializer that uses MessagePack. +/// +public class MsgPackSerializer : ISerializer { - public class MsgPackSerializer : ISerializer - { - /// - /// Any custom resolver to pass to MessagePack. - /// Default is - /// - public IFormatterResolver Resolver { get; private set; } = ContractlessStandardResolver.Instance; + /// + /// Any custom resolver to pass to MessagePack. + /// Default is + /// + public IFormatterResolver Resolver { get; private set; } = ContractlessStandardResolver.Instance; - /// - /// Options for the MessagePack serializer. - /// - public MessagePackSerializerOptions SerializerOptions { get; private set; } = MessagePackSerializerOptions.Standard; + /// + /// Options for the MessagePack serializer. + /// + public MessagePackSerializerOptions SerializerOptions { get; private set; } = MessagePackSerializerOptions.Standard; - /// - /// Creates a new instance of the MessagePack serializer. - /// - /// - /// Custom resolver to pass to MessagePack, default is "Contractless Resolver" - /// (). - /// - public MsgPackSerializer(IFormatterResolver resolver = null) - { - if (resolver != null) - Resolver = resolver; - } + /// + /// Creates a new instance of the MessagePack serializer. + /// + /// + /// Custom resolver to pass to MessagePack, default is "Contractless Resolver" + /// (). + /// + public MsgPackSerializer(IFormatterResolver resolver = null) + { + if (resolver != null) + Resolver = resolver; + } - /// - public TStruct Deserialize(byte[] serialized) - { - return MessagePackSerializer.Deserialize(serialized, SerializerOptions); - } + /// + public TStruct Deserialize(byte[] serialized) + { + return MessagePackSerializer.Deserialize(serialized, SerializerOptions); + } - /// - public byte[] Serialize(ref TStruct item) - { - return MessagePackSerializer.Serialize(item, SerializerOptions); - } + /// + public byte[] Serialize(ref TStruct item) + { + return MessagePackSerializer.Serialize(item, SerializerOptions); } -} +} \ No newline at end of file diff --git a/Source/Reloaded.Messaging.Serializer.MessagePack/Reloaded.Messaging.Serializer.MessagePack.csproj b/Source/Reloaded.Messaging.Serializer.MessagePack/Reloaded.Messaging.Serializer.MessagePack.csproj index 0a3f805..31b6e83 100644 --- a/Source/Reloaded.Messaging.Serializer.MessagePack/Reloaded.Messaging.Serializer.MessagePack.csproj +++ b/Source/Reloaded.Messaging.Serializer.MessagePack/Reloaded.Messaging.Serializer.MessagePack.csproj @@ -2,6 +2,7 @@ netstandard2.1;netstandard2.0 + preview Reloaded.Messaging.Serializer.MessagePack Sewer56 Sewer56 @@ -12,19 +13,10 @@ https://github.com/Reloaded-Project/Reloaded.Messaging true 1.1.0 - true - NuGet-Icon.png - - obj\Reloaded.Messaging.Serializer.MessagePack.xml - - - - obj\Reloaded.Messaging.Serializer.MessagePack.xml - diff --git a/Source/Reloaded.Messaging.Serializer.NewtonsoftJson/NewtonsoftJsonSerializer.cs b/Source/Reloaded.Messaging.Serializer.NewtonsoftJson/NewtonsoftJsonSerializer.cs index 7cc317d..6e9c73e 100644 --- a/Source/Reloaded.Messaging.Serializer.NewtonsoftJson/NewtonsoftJsonSerializer.cs +++ b/Source/Reloaded.Messaging.Serializer.NewtonsoftJson/NewtonsoftJsonSerializer.cs @@ -4,37 +4,36 @@ using Newtonsoft.Json; using Reloaded.Messaging.Interfaces; -namespace Reloaded.Messaging.Serializer.NewtonsoftJson +namespace Reloaded.Messaging.Serializer.NewtonsoftJson; + +/// +/// +/// +public class NewtonsoftJsonSerializer : ISerializer { /// - /// + /// Serialization options. /// - public class NewtonsoftJsonSerializer : ISerializer - { - /// - /// Serialization options. - /// - public JsonSerializerSettings Options { get; private set; } + public JsonSerializerSettings Options { get; private set; } - /// - /// Creates the System.Text.Json based serializer. - /// - /// Options to use for serialization/deserialization. - public NewtonsoftJsonSerializer(JsonSerializerSettings serializerOptions) - { - Options = serializerOptions; - } + /// + /// Creates the System.Text.Json based serializer. + /// + /// Options to use for serialization/deserialization. + public NewtonsoftJsonSerializer(JsonSerializerSettings serializerOptions) + { + Options = serializerOptions; + } - /// - public TStruct Deserialize(byte[] serialized) - { - return JsonConvert.DeserializeObject(Encoding.UTF8.GetString(serialized), Options); - } + /// + public TStruct Deserialize(byte[] serialized) + { + return JsonConvert.DeserializeObject(Encoding.UTF8.GetString(serialized), Options); + } - /// - public byte[] Serialize(ref TStruct item) - { - return Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(item, Options)); - } + /// + public byte[] Serialize(ref TStruct item) + { + return Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(item, Options)); } -} +} \ No newline at end of file diff --git a/Source/Reloaded.Messaging.Serializer.NewtonsoftJson/Reloaded.Messaging.Serializer.NewtonsoftJson.csproj b/Source/Reloaded.Messaging.Serializer.NewtonsoftJson/Reloaded.Messaging.Serializer.NewtonsoftJson.csproj index b9c854b..4331967 100644 --- a/Source/Reloaded.Messaging.Serializer.NewtonsoftJson/Reloaded.Messaging.Serializer.NewtonsoftJson.csproj +++ b/Source/Reloaded.Messaging.Serializer.NewtonsoftJson/Reloaded.Messaging.Serializer.NewtonsoftJson.csproj @@ -2,6 +2,7 @@ netstandard2.0 + preview Reloaded.Messaging.Serializer.NewtonsoftJson Sewer56 Sewer56 diff --git a/Source/Reloaded.Messaging.Serializer.ReloadedMemory/Reloaded.Messaging.Serializer.ReloadedMemory.csproj b/Source/Reloaded.Messaging.Serializer.ReloadedMemory/Reloaded.Messaging.Serializer.ReloadedMemory.csproj index 2563dc5..84f8239 100644 --- a/Source/Reloaded.Messaging.Serializer.ReloadedMemory/Reloaded.Messaging.Serializer.ReloadedMemory.csproj +++ b/Source/Reloaded.Messaging.Serializer.ReloadedMemory/Reloaded.Messaging.Serializer.ReloadedMemory.csproj @@ -2,6 +2,7 @@ netstandard2.1;netstandard2.0 + preview Sewer56 Reloaded.Messaging.Serializer.ReloadedMemory Basic Reloaded.Memory based serialization implementation for Reloaded.Messaging that converts structs to their raw byte representation and back. @@ -10,20 +11,10 @@ https://github.com/Reloaded-Project/Reloaded.Messaging true 1.1.0 - true - NuGet-Icon.png - - obj\Reloaded.Messaging.Serializer.ReloadedMemory.xml - - - - obj\Reloaded.Messaging.Serializer.ReloadedMemory.xml - - diff --git a/Source/Reloaded.Messaging.Serializer.ReloadedMemory/ReloadedMemorySerializer.cs b/Source/Reloaded.Messaging.Serializer.ReloadedMemory/ReloadedMemorySerializer.cs index 83711a9..25b8c59 100644 --- a/Source/Reloaded.Messaging.Serializer.ReloadedMemory/ReloadedMemorySerializer.cs +++ b/Source/Reloaded.Messaging.Serializer.ReloadedMemory/ReloadedMemorySerializer.cs @@ -1,43 +1,42 @@ using Reloaded.Memory; using Reloaded.Messaging.Interfaces; -namespace Reloaded.Messaging.Serializer.ReloadedMemory +namespace Reloaded.Messaging.Serializer.ReloadedMemory; + +/// +/// Serializes messages using raw byte conversion with Reloaded.Memory. +/// +public class ReloadedMemorySerializer : ISerializer { /// - /// Serializes messages using raw byte conversion with Reloaded.Memory. + /// Marshals structures if set to true however is significantly slower. + /// Note: Marshalling also allows you to serialize Classes with [StructLayout] attribute. /// - public class ReloadedMemorySerializer : ISerializer - { - /// - /// Marshals structures if set to true however is significantly slower. - /// Note: Marshalling also allows you to serialize Classes with [StructLayout] attribute. - /// - public bool MarshalValues { get; private set; } + public bool MarshalValues { get; private set; } - /// - /// Creates the Reloaded.Memory based serializer. - /// - /// - /// Marshals structures if set to true however is significantly slower. - /// Note: Marshalling also allows you to serialize Classes with [StructLayout] attribute. - /// - public ReloadedMemorySerializer(bool marshalValues) - { - MarshalValues = marshalValues; - } + /// + /// Creates the Reloaded.Memory based serializer. + /// + /// + /// Marshals structures if set to true however is significantly slower. + /// Note: Marshalling also allows you to serialize Classes with [StructLayout] attribute. + /// + public ReloadedMemorySerializer(bool marshalValues) + { + MarshalValues = marshalValues; + } - /// - public TStruct Deserialize(byte[] serialized) - { - Struct.FromArray(serialized, out TStruct value, MarshalValues, 0); - return value; - } + /// + public TStruct Deserialize(byte[] serialized) + { + Struct.FromArray(serialized, out TStruct value, MarshalValues, 0); + return value; + } - /// - public byte[] Serialize(ref TStruct item) - { - byte[] bytes = Struct.GetBytes(ref item, MarshalValues); - return bytes; - } + /// + public byte[] Serialize(ref TStruct item) + { + byte[] bytes = Struct.GetBytes(ref item, MarshalValues); + return bytes; } -} +} \ No newline at end of file diff --git a/Source/Reloaded.Messaging.Serializer.SystemTextJson/Reloaded.Messaging.Serializer.SystemTextJson.csproj b/Source/Reloaded.Messaging.Serializer.SystemTextJson/Reloaded.Messaging.Serializer.SystemTextJson.csproj index 1fec9d9..fdbae84 100644 --- a/Source/Reloaded.Messaging.Serializer.SystemTextJson/Reloaded.Messaging.Serializer.SystemTextJson.csproj +++ b/Source/Reloaded.Messaging.Serializer.SystemTextJson/Reloaded.Messaging.Serializer.SystemTextJson.csproj @@ -2,6 +2,7 @@ netstandard2.0;net5.0;net7.0 + preview Reloaded.Messaging.Serializer.SystemTextJson Sewer56 Sewer56 diff --git a/Source/Reloaded.Messaging.Serializer.SystemTextJson/SystemTextJsonSerializer.cs b/Source/Reloaded.Messaging.Serializer.SystemTextJson/SystemTextJsonSerializer.cs index 1f28018..992cdba 100644 --- a/Source/Reloaded.Messaging.Serializer.SystemTextJson/SystemTextJsonSerializer.cs +++ b/Source/Reloaded.Messaging.Serializer.SystemTextJson/SystemTextJsonSerializer.cs @@ -2,35 +2,34 @@ using System.Text.Json; using Reloaded.Messaging.Interfaces; -namespace Reloaded.Messaging.Serializer.SystemTextJson +namespace Reloaded.Messaging.Serializer.SystemTextJson; + +/// +public class SystemTextJsonSerializer : ISerializer { - /// - public class SystemTextJsonSerializer : ISerializer - { - /// - /// Serialization options. - /// - public JsonSerializerOptions Options { get; private set; } + /// + /// Serialization options. + /// + public JsonSerializerOptions Options { get; private set; } - /// - /// Creates the System.Text.Json based serializer. - /// - /// Options to use for serialization/deserialization. - public SystemTextJsonSerializer(JsonSerializerOptions serializerOptions) - { - Options = serializerOptions; - } + /// + /// Creates the System.Text.Json based serializer. + /// + /// Options to use for serialization/deserialization. + public SystemTextJsonSerializer(JsonSerializerOptions serializerOptions) + { + Options = serializerOptions; + } - /// - public TStruct Deserialize(byte[] serialized) - { - return JsonSerializer.Deserialize(serialized, Options); - } + /// + public TStruct Deserialize(byte[] serialized) + { + return JsonSerializer.Deserialize(serialized, Options); + } - /// - public byte[] Serialize(ref TStruct item) - { - return JsonSerializer.SerializeToUtf8Bytes(item, Options); - } + /// + public byte[] Serialize(ref TStruct item) + { + return JsonSerializer.SerializeToUtf8Bytes(item, Options); } -} +} \ No newline at end of file diff --git a/Source/Reloaded.Messaging/MessageHandler.cs b/Source/Reloaded.Messaging/MessageHandler.cs index 080237c..a65bbf3 100644 --- a/Source/Reloaded.Messaging/MessageHandler.cs +++ b/Source/Reloaded.Messaging/MessageHandler.cs @@ -3,69 +3,68 @@ using Reloaded.Messaging.Messages; using Reloaded.Messaging.Structs; -namespace Reloaded.Messaging +namespace Reloaded.Messaging; + +/// +/// Provides a generic mechanism for dispatching messages received from a client or server. +/// Works by assigning functions to specified message "types", declared by TMessageType. +/// +/// Type of value to map to individual message handlers. +public class MessageHandler where TMessageType : unmanaged { + private Dictionary _mapping; + + /// + public MessageHandler() + { + _mapping = new Dictionary(); + } + /// - /// Provides a generic mechanism for dispatching messages received from a client or server. - /// Works by assigning functions to specified message "types", declared by TMessageType. + /// Given a raw network message, decodes the message and delegates it to an appropriate handling method. /// - /// Type of value to map to individual message handlers. - public class MessageHandler where TMessageType : unmanaged + public void Handle(ref RawNetMessage parameters) { - private Dictionary _mapping; - - /// - public MessageHandler() - { - _mapping = new Dictionary(); - } - - /// - /// Given a raw network message, decodes the message and delegates it to an appropriate handling method. - /// - public void Handle(ref RawNetMessage parameters) + var messageType = MessageBase.GetMessageType(parameters.Message); + if (_mapping.TryGetValue(messageType, out RawNetMessageHandler value)) { - var messageType = MessageBase.GetMessageType(parameters.Message); - if (_mapping.TryGetValue(messageType, out RawNetMessageHandler value)) - { - value(ref parameters); - } + value(ref parameters); } + } - /// - /// Sets a method to execute handling a specific - /// - public void AddOrOverrideHandler(Handler handler) where TStruct : IMessage, new() - { - var messageType = MessageExtensions.GetMessageType(new TStruct()); - AddOrOverrideHandler(messageType, handler); - } + /// + /// Sets a method to execute handling a specific + /// + public void AddOrOverrideHandler(Handler handler) where TStruct : IMessage, new() + { + var messageType = MessageExtensions.GetMessageType(new TStruct()); + AddOrOverrideHandler(messageType, handler); + } - /// - /// Sets a method to execute handling a specific - /// - public void AddOrOverrideHandler(TMessageType messageType, Handler handler) where TStruct : IMessage, new() + /// + /// Sets a method to execute handling a specific + /// + public void AddOrOverrideHandler(TMessageType messageType, Handler handler) where TStruct : IMessage, new() + { + RawNetMessageHandler parameters = delegate (ref RawNetMessage rawMessage) { - RawNetMessageHandler parameters = delegate (ref RawNetMessage rawMessage) - { - var message = Message.Deserialize(rawMessage.Message); - var netMessage = new NetMessage(ref message, ref rawMessage); - handler(ref netMessage); - }; - - _mapping[messageType] = parameters; - } + var message = Message.Deserialize(rawMessage.Message); + var netMessage = new NetMessage(ref message, ref rawMessage); + handler(ref netMessage); + }; - /// - /// Removes the current method assigned to a handle a message of a specific - /// - public void RemoveHandler(TMessageType messageType) - { - _mapping.Remove(messageType); - } + _mapping[messageType] = parameters; + } - /// - public delegate void Handler(ref NetMessage netMessage); - private delegate void RawNetMessageHandler(ref RawNetMessage rawNetMessage); + /// + /// Removes the current method assigned to a handle a message of a specific + /// + public void RemoveHandler(TMessageType messageType) + { + _mapping.Remove(messageType); } -} + + /// + public delegate void Handler(ref NetMessage netMessage); + private delegate void RawNetMessageHandler(ref RawNetMessage rawNetMessage); +} \ No newline at end of file diff --git a/Source/Reloaded.Messaging/Messages/IMessageExtensions.cs b/Source/Reloaded.Messaging/Messages/IMessageExtensions.cs index cff5ed9..c6b82bc 100644 --- a/Source/Reloaded.Messaging/Messages/IMessageExtensions.cs +++ b/Source/Reloaded.Messaging/Messages/IMessageExtensions.cs @@ -1,20 +1,19 @@ using Reloaded.Messaging.Interfaces; -namespace Reloaded.Messaging.Messages +namespace Reloaded.Messaging.Messages; + +/// +/// Hosts various extension methods related to the interface. +/// +public static class MessageExtensions { /// - /// Hosts various extension methods related to the interface. + /// Retrieves the message type (key) for a given type. /// - public static class MessageExtensions + /// The message to get type for. + /// Used to instantiate and get message type (key) as single liner in source code. + public static TMessageType GetMessageType(this IMessage message) where TMessageType : unmanaged { - /// - /// Retrieves the message type (key) for a given type. - /// - /// The message to get type for. - /// Used to instantiate and get message type (key) as single liner in source code. - public static TMessageType GetMessageType(this IMessage message) where TMessageType : unmanaged - { - return message.GetMessageType(); - } + return message.GetMessageType(); } -} +} \ No newline at end of file diff --git a/Source/Reloaded.Messaging/Messages/Message.cs b/Source/Reloaded.Messaging/Messages/Message.cs index 308a28e..18d85c0 100644 --- a/Source/Reloaded.Messaging/Messages/Message.cs +++ b/Source/Reloaded.Messaging/Messages/Message.cs @@ -3,120 +3,119 @@ using System.Runtime.InteropServices; using Reloaded.Messaging.Interfaces; -namespace Reloaded.Messaging.Messages +namespace Reloaded.Messaging.Messages; + +/// +/// +/// +/// The key for the message. +/// +public unsafe class Message : MessageBase where TStruct : IMessage, new() where TMessageType : unmanaged { + // ReSharper disable StaticMemberInGenericType + private static ISerializer DefaultSerializer { get; set; } + private static ICompressor DefaultCompressor { get; set; } + private static bool DefaultCompressorSet { get; set; } + + // ReSharper restore StaticMemberInGenericType + + /// + /// The actual message backed by this class. + /// + public TStruct ActualMessage { get; set; } + /// /// /// - /// The key for the message. - /// - public unsafe class Message : MessageBase where TStruct : IMessage, new() where TMessageType : unmanaged + /// + public Message(TStruct message) { - // ReSharper disable StaticMemberInGenericType - private static ISerializer DefaultSerializer { get; set; } - private static ICompressor DefaultCompressor { get; set; } - private static bool DefaultCompressorSet { get; set; } - - // ReSharper restore StaticMemberInGenericType - - /// - /// The actual message backed by this class. - /// - public TStruct ActualMessage { get; set; } - - /// - /// - /// - /// - public Message(TStruct message) - { - ActualMessage = message; - } + ActualMessage = message; + } - /// - /// Serializes the current instance and returns an array of bytes representing the instance. - /// - public byte[] Serialize() - { - // Perform serialization. - var serializer = GetSerializer(); - var message = ActualMessage; - var encodedMessage = serializer.Serialize(ref message); + /// + /// Serializes the current instance and returns an array of bytes representing the instance. + /// + public byte[] Serialize() + { + // Perform serialization. + var serializer = GetSerializer(); + var message = ActualMessage; + var encodedMessage = serializer.Serialize(ref message); - // Allocate memory for result and write header. - var result = new byte[encodedMessage.Length + sizeof(TMessageType)]; - var resultSpan = result.AsSpan(); - var messageType = ActualMessage.GetMessageType(); + // Allocate memory for result and write header. + var result = new byte[encodedMessage.Length + sizeof(TMessageType)]; + var resultSpan = result.AsSpan(); + var messageType = ActualMessage.GetMessageType(); #if (USE_NATIVE_SPAN_API) - var readOnlyMessageType = MemoryMarshal.CreateReadOnlySpan(ref messageType, sizeof(TMessageType)); - var readOnlyMessageTypeBytes = MemoryMarshal.AsBytes(readOnlyMessageType); - readOnlyMessageTypeBytes.CopyTo(resultSpan); + var readOnlyMessageType = MemoryMarshal.CreateReadOnlySpan(ref messageType, sizeof(TMessageType)); + var readOnlyMessageTypeBytes = MemoryMarshal.AsBytes(readOnlyMessageType); + readOnlyMessageTypeBytes.CopyTo(resultSpan); #else byte* bytes = (byte*)Unsafe.AsPointer(ref messageType); var readOnlyMessageTypeBytes = new Span(bytes, sizeof(TMessageType)); readOnlyMessageTypeBytes.CopyTo(resultSpan); #endif - // Append serialized data. - resultSpan = resultSpan.Slice(sizeof(TMessageType)); - encodedMessage.AsSpan().CopyTo(resultSpan); + // Append serialized data. + resultSpan = resultSpan.Slice(sizeof(TMessageType)); + encodedMessage.AsSpan().CopyTo(resultSpan); - var compressor = GetCompressor(); - if (compressor != null) - result = compressor.Compress(result); - - return result; - } + var compressor = GetCompressor(); + if (compressor != null) + result = compressor.Compress(result); - /// - /// Deserializes a given set of bytes into a usable struct. - /// - public static TStruct Deserialize(byte[] serializedBytes) - { - // Get decompressor. - var compressor = GetCompressor(); - if (compressor != null) - serializedBytes = compressor.Decompress(serializedBytes); + return result; + } - // Get serializer - var serializer = GetSerializer(); + /// + /// Deserializes a given set of bytes into a usable struct. + /// + public static TStruct Deserialize(byte[] serializedBytes) + { + // Get decompressor. + var compressor = GetCompressor(); + if (compressor != null) + serializedBytes = compressor.Decompress(serializedBytes); - // Read message. - var messageSegment = serializedBytes.AsSpan(sizeof(TMessageType)).ToArray(); - var message = serializer.Deserialize(messageSegment); - return message; + // Get serializer + var serializer = GetSerializer(); - // Note: No need to read MessageType. MessageType was only necessary to link a message to correct handler. - } + // Read message. + var messageSegment = serializedBytes.AsSpan(sizeof(TMessageType)).ToArray(); + var message = serializer.Deserialize(messageSegment); + return message; - private static ISerializer GetSerializer() - { - if (Overrides.SerializerOverride.TryGetValue(typeof(TStruct), out ISerializer value)) - return value; + // Note: No need to read MessageType. MessageType was only necessary to link a message to correct handler. + } - if (DefaultSerializer == null) - { - var defaultStruct = new TStruct(); - DefaultSerializer = ((IMessage)defaultStruct).GetSerializer(); - } + private static ISerializer GetSerializer() + { + if (Overrides.SerializerOverride.TryGetValue(typeof(TStruct), out ISerializer value)) + return value; - return DefaultSerializer; + if (DefaultSerializer == null) + { + var defaultStruct = new TStruct(); + DefaultSerializer = ((IMessage)defaultStruct).GetSerializer(); } - private static ICompressor GetCompressor() - { - if (Overrides.CompressorOverride.TryGetValue(typeof(TStruct), out ICompressor value)) - return value; + return DefaultSerializer; + } - if (! DefaultCompressorSet) - { - var defaultStruct = new TStruct(); - DefaultCompressor = ((IMessage)defaultStruct).GetCompressor(); - DefaultCompressorSet = true; - } + private static ICompressor GetCompressor() + { + if (Overrides.CompressorOverride.TryGetValue(typeof(TStruct), out ICompressor value)) + return value; - return DefaultCompressor; + if (! DefaultCompressorSet) + { + var defaultStruct = new TStruct(); + DefaultCompressor = ((IMessage)defaultStruct).GetCompressor(); + DefaultCompressorSet = true; } + + return DefaultCompressor; } -} +} \ No newline at end of file diff --git a/Source/Reloaded.Messaging/Messages/MessageBase.cs b/Source/Reloaded.Messaging/Messages/MessageBase.cs index 9889986..1e324b7 100644 --- a/Source/Reloaded.Messaging/Messages/MessageBase.cs +++ b/Source/Reloaded.Messaging/Messages/MessageBase.cs @@ -1,22 +1,21 @@ -namespace Reloaded.Messaging.Messages +namespace Reloaded.Messaging.Messages; + +/// +/// The base class for the class, used for dealing with message type specific things. +/// +/// +public unsafe class MessageBase where TMessageType : unmanaged { /// - /// The base class for the class, used for dealing with message type specific things. + /// Retrieves the message type by raw array of values. /// - /// - public unsafe class MessageBase where TMessageType : unmanaged + /// The raw serialized bytes containing the message. + /// The type of the message. + public static TMessageType GetMessageType(byte[] serializedBytes) { - /// - /// Retrieves the message type by raw array of values. - /// - /// The raw serialized bytes containing the message. - /// The type of the message. - public static TMessageType GetMessageType(byte[] serializedBytes) + fixed (byte* arrayPtr = serializedBytes) { - fixed (byte* arrayPtr = serializedBytes) - { - return *(TMessageType*) arrayPtr; - } + return *(TMessageType*) arrayPtr; } } -} +} \ No newline at end of file diff --git a/Source/Reloaded.Messaging/Overrides.cs b/Source/Reloaded.Messaging/Overrides.cs index 8bc95dc..979fe51 100644 --- a/Source/Reloaded.Messaging/Overrides.cs +++ b/Source/Reloaded.Messaging/Overrides.cs @@ -2,22 +2,21 @@ using System.Collections.Generic; using Reloaded.Messaging.Interfaces; -namespace Reloaded.Messaging +namespace Reloaded.Messaging; + +/// +/// Class which provides, client-side the ability to override serializers and compressors for specified types. +/// Use either for testing or benchmarking. +/// +public static class Overrides { /// - /// Class which provides, client-side the ability to override serializers and compressors for specified types. - /// Use either for testing or benchmarking. + /// Allows you to override the serializer for a specific type. /// - public static class Overrides - { - /// - /// Allows you to override the serializer for a specific type. - /// - public static Dictionary SerializerOverride { get; } = new Dictionary(); + public static Dictionary SerializerOverride { get; } = new Dictionary(); - /// - /// Allows you to override the compressor for a specific type. - /// - public static Dictionary CompressorOverride { get; } = new Dictionary(); - } -} + /// + /// Allows you to override the compressor for a specific type. + /// + public static Dictionary CompressorOverride { get; } = new Dictionary(); +} \ No newline at end of file diff --git a/Source/Reloaded.Messaging/Reloaded.Messaging.csproj b/Source/Reloaded.Messaging/Reloaded.Messaging.csproj index d79a67b..1e8bcf5 100644 --- a/Source/Reloaded.Messaging/Reloaded.Messaging.csproj +++ b/Source/Reloaded.Messaging/Reloaded.Messaging.csproj @@ -3,6 +3,7 @@ netstandard2.1;netstandard2.0;netcoreapp3.1;net5.0 false + preview Sewer56 Reloaded (Mod Loader) II's extensible "event-like" solution for passing messages across a local or remote network that sits ontop of LiteNetLib. diff --git a/Source/Reloaded.Messaging/SimpleHost.cs b/Source/Reloaded.Messaging/SimpleHost.cs index 2f1e599..204896c 100644 --- a/Source/Reloaded.Messaging/SimpleHost.cs +++ b/Source/Reloaded.Messaging/SimpleHost.cs @@ -2,84 +2,83 @@ using LiteNetLib; using Reloaded.Messaging.Structs; -namespace Reloaded.Messaging +namespace Reloaded.Messaging; + +/// +/// Provides a simple client or host based off of LiteNetLib. +/// +public class SimpleHost : IDisposable where TMessageType : unmanaged { /// - /// Provides a simple client or host based off of LiteNetLib. + /// The password necessary to join this host. If it does not match, incoming clients will be rejected. /// - public class SimpleHost : IDisposable where TMessageType : unmanaged - { - /// - /// The password necessary to join this host. If it does not match, incoming clients will be rejected. - /// - public string Password { get; set; } + public string Password { get; set; } - /// - /// Set to true to accept incoming clients, else reject all clients. - /// - public bool AcceptClients { get; set; } + /// + /// Set to true to accept incoming clients, else reject all clients. + /// + public bool AcceptClients { get; set; } - /// - /// Event for handling connection requests. - /// - public event EventBasedNetListener.OnConnectionRequest ConnectionRequestEvent; + /// + /// Event for handling connection requests. + /// + public event EventBasedNetListener.OnConnectionRequest ConnectionRequestEvent; - /// - /// Dispatcher for individual (s) to your events. - /// - public MessageHandler MessageHandler { get; private set; } + /// + /// Dispatcher for individual (s) to your events. + /// + public MessageHandler MessageHandler { get; private set; } - /// - public EventBasedNetListener Listener { get; private set; } + /// + public EventBasedNetListener Listener { get; private set; } - /// - public NetManager NetManager { get; private set; } + /// + public NetManager NetManager { get; private set; } - /// - /// Provides a simple client or host based off of LiteNetLib. - /// - /// Set to true to accept incoming clients, else reject all requests. - /// The password necessary to join. - public SimpleHost(bool acceptClients, string password = "") - { - Password = password; - AcceptClients = acceptClients; - MessageHandler = new MessageHandler(); - Listener = new EventBasedNetListener(); - Listener.NetworkReceiveEvent += OnNetworkReceive; - Listener.ConnectionRequestEvent += ListenerOnConnectionRequestEvent; + /// + /// Provides a simple client or host based off of LiteNetLib. + /// + /// Set to true to accept incoming clients, else reject all requests. + /// The password necessary to join. + public SimpleHost(bool acceptClients, string password = "") + { + Password = password; + AcceptClients = acceptClients; + MessageHandler = new MessageHandler(); + Listener = new EventBasedNetListener(); + Listener.NetworkReceiveEvent += OnNetworkReceive; + Listener.ConnectionRequestEvent += ListenerOnConnectionRequestEvent; - NetManager = new NetManager(Listener); - NetManager.UnsyncedEvents = true; - NetManager.AutoRecycle = true; - } + NetManager = new NetManager(Listener); + NetManager.UnsyncedEvents = true; + NetManager.AutoRecycle = true; + } - /// - public void Dispose() - { - NetManager.Stop(); - } + /// + public void Dispose() + { + NetManager.Stop(); + } - private void ListenerOnConnectionRequestEvent(ConnectionRequest request) + private void ListenerOnConnectionRequestEvent(ConnectionRequest request) + { + if (ConnectionRequestEvent != null) { - if (ConnectionRequestEvent != null) - { - ConnectionRequestEvent(request); - return; - } - - if (AcceptClients) - request.AcceptIfKey(Password); - else - request.Reject(); + ConnectionRequestEvent(request); + return; } - /* On each message received. */ - private void OnNetworkReceive(NetPeer peer, NetPacketReader reader, byte channel, DeliveryMethod deliverymethod) - { - byte[] rawBytes = reader.GetRemainingBytes(); - var rawNetMessage = new RawNetMessage(rawBytes, peer, reader, deliverymethod); - MessageHandler.Handle(ref rawNetMessage); - } + if (AcceptClients) + request.AcceptIfKey(Password); + else + request.Reject(); + } + + /* On each message received. */ + private void OnNetworkReceive(NetPeer peer, NetPacketReader reader, byte channel, DeliveryMethod deliverymethod) + { + byte[] rawBytes = reader.GetRemainingBytes(); + var rawNetMessage = new RawNetMessage(rawBytes, peer, reader, deliverymethod); + MessageHandler.Handle(ref rawNetMessage); } -} +} \ No newline at end of file diff --git a/Source/Reloaded.Messaging/Structs/NetMessage.cs b/Source/Reloaded.Messaging/Structs/NetMessage.cs index b4c8a30..4163786 100644 --- a/Source/Reloaded.Messaging/Structs/NetMessage.cs +++ b/Source/Reloaded.Messaging/Structs/NetMessage.cs @@ -1,43 +1,42 @@ using LiteNetLib; -namespace Reloaded.Messaging.Structs +namespace Reloaded.Messaging.Structs; + +/// +/// Encapsulates a message with a specific structure received from the network as a raw array of bytes. +/// +public struct NetMessage { /// - /// Encapsulates a message with a specific structure received from the network as a raw array of bytes. + /// The message received from the peer. /// - public struct NetMessage - { - /// - /// The message received from the peer. - /// - public TStruct Message { get; private set; } + public TStruct Message { get; private set; } - /// - /// The peer which has sent you the message. - /// - public NetPeer Peer { get; private set; } + /// + /// The peer which has sent you the message. + /// + public NetPeer Peer { get; private set; } - /// - /// Can be used to read the message, if desired. - /// - public NetPacketReader PacketReader { get; private set; } + /// + /// Can be used to read the message, if desired. + /// + public NetPacketReader PacketReader { get; private set; } - /// - /// The method via which the package was delivered. - /// - public DeliveryMethod DeliveryMethod { get; private set; } + /// + /// The method via which the package was delivered. + /// + public DeliveryMethod DeliveryMethod { get; private set; } - /// - /// Encapsulates a raw message received from the network. - /// - /// The message in question. - /// The raw message from which this message should be constructed with. - public NetMessage(ref TStruct message, ref RawNetMessage rawMessage) - { - Message = message; - Peer = rawMessage.Peer; - PacketReader = rawMessage.PacketReader; - DeliveryMethod = rawMessage.DeliveryMethod; - } + /// + /// Encapsulates a raw message received from the network. + /// + /// The message in question. + /// The raw message from which this message should be constructed with. + public NetMessage(ref TStruct message, ref RawNetMessage rawMessage) + { + Message = message; + Peer = rawMessage.Peer; + PacketReader = rawMessage.PacketReader; + DeliveryMethod = rawMessage.DeliveryMethod; } -} +} \ No newline at end of file diff --git a/Source/Reloaded.Messaging/Structs/RawNetMessage.cs b/Source/Reloaded.Messaging/Structs/RawNetMessage.cs index 87e7f65..7240ccb 100644 --- a/Source/Reloaded.Messaging/Structs/RawNetMessage.cs +++ b/Source/Reloaded.Messaging/Structs/RawNetMessage.cs @@ -1,45 +1,44 @@ using LiteNetLib; -namespace Reloaded.Messaging.Structs +namespace Reloaded.Messaging.Structs; + +/// +/// Encapsulates a message received from the network as a raw array of bytes. +/// +public struct RawNetMessage { /// - /// Encapsulates a message received from the network as a raw array of bytes. + /// The contents of the message. /// - public struct RawNetMessage - { - /// - /// The contents of the message. - /// - public byte[] Message { get; private set; } + public byte[] Message { get; private set; } - /// - /// The peer from whom the message was received from. - /// - public NetPeer Peer { get; private set; } + /// + /// The peer from whom the message was received from. + /// + public NetPeer Peer { get; private set; } - /// - /// Used to read the packet internals, if desired. - /// - public NetPacketReader PacketReader { get; private set; } + /// + /// Used to read the packet internals, if desired. + /// + public NetPacketReader PacketReader { get; private set; } - /// - /// The method via which this message was delivered. - /// - public DeliveryMethod DeliveryMethod { get; private set; } + /// + /// The method via which this message was delivered. + /// + public DeliveryMethod DeliveryMethod { get; private set; } - /// - /// Encapsulates the message received from the network as a raw array of bytes. - /// - /// The message in question. - /// The peer from whom the message was received from. - /// Used to read packet internals, if desired. - /// The method via which the message was delivered. - public RawNetMessage(byte[] message, NetPeer peer, NetPacketReader packetReader, DeliveryMethod deliveryMethod) - { - Message = message; - Peer = peer; - PacketReader = packetReader; - DeliveryMethod = deliveryMethod; - } + /// + /// Encapsulates the message received from the network as a raw array of bytes. + /// + /// The message in question. + /// The peer from whom the message was received from. + /// Used to read packet internals, if desired. + /// The method via which the message was delivered. + public RawNetMessage(byte[] message, NetPeer peer, NetPacketReader packetReader, DeliveryMethod deliveryMethod) + { + Message = message; + Peer = peer; + PacketReader = packetReader; + DeliveryMethod = deliveryMethod; } -} +} \ No newline at end of file From f9440ec72f0527c0157c4a288154965a51cc8c61 Mon Sep 17 00:00:00 2001 From: Sewer 56 Date: Wed, 6 Jul 2022 00:39:17 +0100 Subject: [PATCH 07/16] Added: File Scoped Namespaces for Unit Tests --- .../Init/TestingHosts.cs | 43 +++---- .../Reloaded.Messaging.Tests/MessageType.cs | 13 +- .../Reloaded.Messaging.Tests.csproj | 2 +- .../Struct/StringMessage.cs | 49 ++++--- .../Struct/Vector3.cs | 79 ++++++------ .../Tests/Compression/CompressionTest.cs | 69 +++++----- .../Tests/Serialization/PureSerialize.cs | 25 ++-- .../Tests/Serialization/StringPassTest.cs | 121 +++++++++--------- .../Tests/Serialization/VectorPassTest.cs | 121 +++++++++--------- 9 files changed, 257 insertions(+), 265 deletions(-) diff --git a/Source/Reloaded.Messaging.Tests/Init/TestingHosts.cs b/Source/Reloaded.Messaging.Tests/Init/TestingHosts.cs index 58f5a5a..5eb9c43 100644 --- a/Source/Reloaded.Messaging.Tests/Init/TestingHosts.cs +++ b/Source/Reloaded.Messaging.Tests/Init/TestingHosts.cs @@ -1,33 +1,32 @@ using System; using System.Net; -namespace Reloaded.Messaging.Tests.Init +namespace Reloaded.Messaging.Tests.Init; + +public class TestingHosts : IDisposable { - public class TestingHosts : IDisposable - { - private const string DefaultPassword = "CutenessIsJustice"; - public SimpleHost SimpleServer; - public SimpleHost SimpleClient; + private const string DefaultPassword = "CutenessIsJustice"; + public SimpleHost SimpleServer; + public SimpleHost SimpleClient; - public TestingHosts() - { - SimpleServer = new SimpleHost(true, DefaultPassword); - SimpleClient = new SimpleHost(false, DefaultPassword); + public TestingHosts() + { + SimpleServer = new SimpleHost(true, DefaultPassword); + SimpleClient = new SimpleHost(false, DefaultPassword); - SimpleServer.NetManager.Start(IPAddress.Loopback, IPAddress.IPv6Loopback, 0); - SimpleClient.NetManager.Start(IPAddress.Loopback, IPAddress.IPv6Loopback, 0); - SimpleClient.NetManager.Connect(new IPEndPoint(IPAddress.Loopback, SimpleServer.NetManager.LocalPort), DefaultPassword); + SimpleServer.NetManager.Start(IPAddress.Loopback, IPAddress.IPv6Loopback, 0); + SimpleClient.NetManager.Start(IPAddress.Loopback, IPAddress.IPv6Loopback, 0); + SimpleClient.NetManager.Connect(new IPEndPoint(IPAddress.Loopback, SimpleServer.NetManager.LocalPort), DefaultPassword); #if DEBUG - SimpleServer.NetManager.DisconnectTimeout = int.MaxValue; - SimpleClient.NetManager.DisconnectTimeout = int.MaxValue; + SimpleServer.NetManager.DisconnectTimeout = int.MaxValue; + SimpleClient.NetManager.DisconnectTimeout = int.MaxValue; #endif - } + } - public void Dispose() - { - SimpleServer?.Dispose(); - SimpleClient?.Dispose(); - } + public void Dispose() + { + SimpleServer?.Dispose(); + SimpleClient?.Dispose(); } -} +} \ No newline at end of file diff --git a/Source/Reloaded.Messaging.Tests/MessageType.cs b/Source/Reloaded.Messaging.Tests/MessageType.cs index 686c815..2d16aef 100644 --- a/Source/Reloaded.Messaging.Tests/MessageType.cs +++ b/Source/Reloaded.Messaging.Tests/MessageType.cs @@ -1,8 +1,7 @@ -namespace Reloaded.Messaging.Tests +namespace Reloaded.Messaging.Tests; + +public enum MessageType : byte { - public enum MessageType : byte - { - String, - Vector3 - } -} + String, + Vector3 +} \ No newline at end of file diff --git a/Source/Reloaded.Messaging.Tests/Reloaded.Messaging.Tests.csproj b/Source/Reloaded.Messaging.Tests/Reloaded.Messaging.Tests.csproj index a51d27b..0ede4a0 100644 --- a/Source/Reloaded.Messaging.Tests/Reloaded.Messaging.Tests.csproj +++ b/Source/Reloaded.Messaging.Tests/Reloaded.Messaging.Tests.csproj @@ -2,7 +2,7 @@ netcoreapp3.1;NET472;net5.0 - + preview false diff --git a/Source/Reloaded.Messaging.Tests/Struct/StringMessage.cs b/Source/Reloaded.Messaging.Tests/Struct/StringMessage.cs index 820d475..27d8032 100644 --- a/Source/Reloaded.Messaging.Tests/Struct/StringMessage.cs +++ b/Source/Reloaded.Messaging.Tests/Struct/StringMessage.cs @@ -3,35 +3,34 @@ using Reloaded.Messaging.Messages; using Reloaded.Messaging.Serializer.MessagePack; -namespace Reloaded.Messaging.Tests.Struct +namespace Reloaded.Messaging.Tests.Struct; + +public struct StringMessage : IMessage { - public struct StringMessage : IMessage - { - public MessageType GetMessageType() => MessageType.String; - public ISerializer GetSerializer() => new MsgPackSerializer(); - public ICompressor GetCompressor() => null; + public MessageType GetMessageType() => MessageType.String; + public ISerializer GetSerializer() => new MsgPackSerializer(); + public ICompressor GetCompressor() => null; - public string Text { get; set; } + public string Text { get; set; } - public StringMessage(string text) - { - Text = text; - } + public StringMessage(string text) + { + Text = text; + } - /* Auto Generated by R# */ - public bool Equals(StringMessage other) - { - return string.Equals(Text, other.Text); - } + /* Auto Generated by R# */ + public bool Equals(StringMessage other) + { + return string.Equals(Text, other.Text); + } - public override bool Equals(object obj) - { - return obj is StringMessage other && Equals(other); - } + public override bool Equals(object obj) + { + return obj is StringMessage other && Equals(other); + } - public override int GetHashCode() - { - return (Text != null ? Text.GetHashCode() : 0); - } + public override int GetHashCode() + { + return (Text != null ? Text.GetHashCode() : 0); } -} +} \ No newline at end of file diff --git a/Source/Reloaded.Messaging.Tests/Struct/Vector3.cs b/Source/Reloaded.Messaging.Tests/Struct/Vector3.cs index ad1d821..360e24d 100644 --- a/Source/Reloaded.Messaging.Tests/Struct/Vector3.cs +++ b/Source/Reloaded.Messaging.Tests/Struct/Vector3.cs @@ -2,55 +2,54 @@ using Reloaded.Messaging.Messages; using Reloaded.Messaging.Serializer.MessagePack; -namespace Reloaded.Messaging.Tests.Struct +namespace Reloaded.Messaging.Tests.Struct; + +public struct Vector3 : IMessage { - public struct Vector3 : IMessage - { - public MessageType GetMessageType() => MessageType.Vector3; - public ISerializer GetSerializer() => new MsgPackSerializer(); - public ICompressor GetCompressor() => null; + public MessageType GetMessageType() => MessageType.Vector3; + public ISerializer GetSerializer() => new MsgPackSerializer(); + public ICompressor GetCompressor() => null; - public float X { get; set; } - public float Y { get; set; } - public float Z { get; set; } + public float X { get; set; } + public float Y { get; set; } + public float Z { get; set; } - public Vector3(float x, float y, float z) - { - X = x; - Y = y; - Z = z; - } + public Vector3(float x, float y, float z) + { + X = x; + Y = y; + Z = z; + } - /* Auto-implemented by R# */ - private bool Equals(Vector3 other) - { - return X.Equals(other.X) && Y.Equals(other.Y) && Z.Equals(other.Z); - } + /* Auto-implemented by R# */ + private bool Equals(Vector3 other) + { + return X.Equals(other.X) && Y.Equals(other.Y) && Z.Equals(other.Z); + } - public override bool Equals(object obj) - { - if (ReferenceEquals(null, obj)) - return false; + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + return false; - if (ReferenceEquals(this, obj)) - return true; + if (ReferenceEquals(this, obj)) + return true; - if (obj.GetType() != this.GetType()) - return false; + if (obj.GetType() != this.GetType()) + return false; - return Equals((Vector3)obj); - } + return Equals((Vector3)obj); + } - public override int GetHashCode() + public override int GetHashCode() + { + unchecked { - unchecked - { - var hashCode = X.GetHashCode(); - hashCode = (hashCode * 397) ^ Y.GetHashCode(); - hashCode = (hashCode * 397) ^ Z.GetHashCode(); - return hashCode; - } + var hashCode = X.GetHashCode(); + hashCode = (hashCode * 397) ^ Y.GetHashCode(); + hashCode = (hashCode * 397) ^ Z.GetHashCode(); + return hashCode; } - } -} + +} \ No newline at end of file diff --git a/Source/Reloaded.Messaging.Tests/Tests/Compression/CompressionTest.cs b/Source/Reloaded.Messaging.Tests/Tests/Compression/CompressionTest.cs index 4159ce0..8542aa6 100644 --- a/Source/Reloaded.Messaging.Tests/Tests/Compression/CompressionTest.cs +++ b/Source/Reloaded.Messaging.Tests/Tests/Compression/CompressionTest.cs @@ -6,41 +6,40 @@ using Xunit; using ZstdNet; -namespace Reloaded.Messaging.Tests.Tests.Compression +namespace Reloaded.Messaging.Tests.Tests.Compression; + +public class CompressionTest : IDisposable { - public class CompressionTest : IDisposable + public void Dispose() { - public void Dispose() - { - Overrides.SerializerOverride.Remove(typeof(Vector3)); - Overrides.CompressorOverride.Remove(typeof(Vector3)); - } - - [Fact] - public void CheckCompression() - { - var originalVector = new Vector3(235F, 10F, 5F); - var vectorMessage = new Message(originalVector); - - // Set serialization: Reloaded. - // Set compression: None - GC.Collect(); - Overrides.SerializerOverride[typeof(Vector3)] = new ReloadedMemorySerializer(false); - Overrides.CompressorOverride.Remove(typeof(Vector3)); - - var serializedUncompressed = vectorMessage.Serialize(); - var deserializedUncompressed = Message.Deserialize(serializedUncompressed); - Assert.Equal(originalVector, deserializedUncompressed); - - // Set compression: ZStandard - Overrides.CompressorOverride[typeof(Vector3)] = new ZStandardCompressor(new CompressionOptions(22)); - var serializedZStandard = vectorMessage.Serialize(); - var deserializedZStandard = Message.Deserialize(serializedZStandard); - - Assert.Equal(originalVector, deserializedZStandard); - Assert.NotEqual(serializedUncompressed, serializedZStandard); - - // Note: Compression here acts negatively. - } + Overrides.SerializerOverride.Remove(typeof(Vector3)); + Overrides.CompressorOverride.Remove(typeof(Vector3)); + } + + [Fact] + public void CheckCompression() + { + var originalVector = new Vector3(235F, 10F, 5F); + var vectorMessage = new Message(originalVector); + + // Set serialization: Reloaded. + // Set compression: None + GC.Collect(); + Overrides.SerializerOverride[typeof(Vector3)] = new ReloadedMemorySerializer(false); + Overrides.CompressorOverride.Remove(typeof(Vector3)); + + var serializedUncompressed = vectorMessage.Serialize(); + var deserializedUncompressed = Message.Deserialize(serializedUncompressed); + Assert.Equal(originalVector, deserializedUncompressed); + + // Set compression: ZStandard + Overrides.CompressorOverride[typeof(Vector3)] = new ZStandardCompressor(new CompressionOptions(22)); + var serializedZStandard = vectorMessage.Serialize(); + var deserializedZStandard = Message.Deserialize(serializedZStandard); + + Assert.Equal(originalVector, deserializedZStandard); + Assert.NotEqual(serializedUncompressed, serializedZStandard); + + // Note: Compression here acts negatively. } -} +} \ No newline at end of file diff --git a/Source/Reloaded.Messaging.Tests/Tests/Serialization/PureSerialize.cs b/Source/Reloaded.Messaging.Tests/Tests/Serialization/PureSerialize.cs index 80def5e..23c2bca 100644 --- a/Source/Reloaded.Messaging.Tests/Tests/Serialization/PureSerialize.cs +++ b/Source/Reloaded.Messaging.Tests/Tests/Serialization/PureSerialize.cs @@ -5,20 +5,19 @@ using Reloaded.Messaging.Tests.Struct; using Xunit; -namespace Reloaded.Messaging.Tests.Tests.Serialization +namespace Reloaded.Messaging.Tests.Tests.Serialization; + +public class PureSerialize { - public class PureSerialize - { - /* Tests Pure Serialization: No Message Passing involved. */ + /* Tests Pure Serialization: No Message Passing involved. */ - [Fact] - public void SerializeAndDeserialize() - { - var vector = new Vector3(0, 25, 100); - byte[] data = vector.Serialize(); - var newVector = Serializable.Deserialize(data); - Assert.Equal(vector, newVector); - } + [Fact] + public void SerializeAndDeserialize() + { + var vector = new Vector3(0, 25, 100); + byte[] data = vector.Serialize(); + var newVector = Serializable.Deserialize(data); + Assert.Equal(vector, newVector); } -} +} \ No newline at end of file diff --git a/Source/Reloaded.Messaging.Tests/Tests/Serialization/StringPassTest.cs b/Source/Reloaded.Messaging.Tests/Tests/Serialization/StringPassTest.cs index 45c19cd..f30512c 100644 --- a/Source/Reloaded.Messaging.Tests/Tests/Serialization/StringPassTest.cs +++ b/Source/Reloaded.Messaging.Tests/Tests/Serialization/StringPassTest.cs @@ -14,79 +14,78 @@ using Reloaded.Messaging.Tests.Struct; using Xunit; -namespace Reloaded.Messaging.Tests.Tests.Serialization +namespace Reloaded.Messaging.Tests.Tests.Serialization; + +public class StringPassTest : IDisposable { - public class StringPassTest : IDisposable + private const string Message = "Test Message"; + private TestingHosts _hosts; + + public StringPassTest() { - private const string Message = "Test Message"; - private TestingHosts _hosts; + _hosts = new TestingHosts(); + } - public StringPassTest() - { - _hosts = new TestingHosts(); - } + public void Dispose() + { + Overrides.SerializerOverride.Remove(typeof(StringMessage)); + Overrides.CompressorOverride.Remove(typeof(StringMessage)); + _hosts.Dispose(); + } - public void Dispose() - { - Overrides.SerializerOverride.Remove(typeof(StringMessage)); - Overrides.CompressorOverride.Remove(typeof(StringMessage)); - _hosts.Dispose(); - } + [Fact(Timeout = 1000)] + public void MsgPackPassString() + { + Overrides.SerializerOverride[typeof(StringMessage)] = new MsgPackSerializer(); + Overrides.CompressorOverride[typeof(StringMessage)] = null; + PassString(); + } - [Fact(Timeout = 1000)] - public void MsgPackPassString() - { - Overrides.SerializerOverride[typeof(StringMessage)] = new MsgPackSerializer(); - Overrides.CompressorOverride[typeof(StringMessage)] = null; - PassString(); - } + [Fact(Timeout = 1000)] + public void ReloadedPassString() + { + Overrides.SerializerOverride[typeof(StringMessage)] = new ReloadedMemorySerializer(true); + Overrides.CompressorOverride[typeof(StringMessage)] = null; + PassString(); + } - [Fact(Timeout = 1000)] - public void ReloadedPassString() - { - Overrides.SerializerOverride[typeof(StringMessage)] = new ReloadedMemorySerializer(true); - Overrides.CompressorOverride[typeof(StringMessage)] = null; - PassString(); - } + [Fact(Timeout = 1000)] + public void SystemTextJsonPassString() + { + Overrides.SerializerOverride[typeof(StringMessage)] = new SystemTextJsonSerializer(new JsonSerializerOptions()); + Overrides.CompressorOverride[typeof(StringMessage)] = null; + PassString(); + } - [Fact(Timeout = 1000)] - public void SystemTextJsonPassString() - { - Overrides.SerializerOverride[typeof(StringMessage)] = new SystemTextJsonSerializer(new JsonSerializerOptions()); - Overrides.CompressorOverride[typeof(StringMessage)] = null; - PassString(); - } + [Fact(Timeout = 1000)] + public void NewtonsoftPassString() + { + Overrides.SerializerOverride[typeof(StringMessage)] = new NewtonsoftJsonSerializer(new JsonSerializerSettings()); + Overrides.CompressorOverride[typeof(StringMessage)] = null; + PassString(); + } - [Fact(Timeout = 1000)] - public void NewtonsoftPassString() + private void PassString() + { + string delivered = default; + + // Message handling method + void Handler(ref NetMessage netMessage) { - Overrides.SerializerOverride[typeof(StringMessage)] = new NewtonsoftJsonSerializer(new JsonSerializerSettings()); - Overrides.CompressorOverride[typeof(StringMessage)] = null; - PassString(); + delivered = netMessage.Message.Text; } - private void PassString() - { - string delivered = default; - - // Message handling method - void Handler(ref NetMessage netMessage) - { - delivered = netMessage.Message.Text; - } - - // Setup client. - _hosts.SimpleClient.MessageHandler.AddOrOverrideHandler(Handler); + // Setup client. + _hosts.SimpleClient.MessageHandler.AddOrOverrideHandler(Handler); - // Send Message. - var stringMessage = new Message(new StringMessage(Message)); - var data = stringMessage.Serialize(); - _hosts.SimpleServer.NetManager.FirstPeer.Send(data, DeliveryMethod.ReliableOrdered); + // Send Message. + var stringMessage = new Message(new StringMessage(Message)); + var data = stringMessage.Serialize(); + _hosts.SimpleServer.NetManager.FirstPeer.Send(data, DeliveryMethod.ReliableOrdered); - while (delivered == default) - Thread.Sleep(16); + while (delivered == default) + Thread.Sleep(16); - Assert.Equal(Message, delivered); - } + Assert.Equal(Message, delivered); } -} +} \ No newline at end of file diff --git a/Source/Reloaded.Messaging.Tests/Tests/Serialization/VectorPassTest.cs b/Source/Reloaded.Messaging.Tests/Tests/Serialization/VectorPassTest.cs index 384eba9..a3262c0 100644 --- a/Source/Reloaded.Messaging.Tests/Tests/Serialization/VectorPassTest.cs +++ b/Source/Reloaded.Messaging.Tests/Tests/Serialization/VectorPassTest.cs @@ -13,79 +13,78 @@ using Xunit; using Vector3 = Reloaded.Messaging.Tests.Struct.Vector3; -namespace Reloaded.Messaging.Tests.Tests.Serialization +namespace Reloaded.Messaging.Tests.Tests.Serialization; + +public class VectorPassTest : IDisposable { - public class VectorPassTest : IDisposable + private TestingHosts _hosts; + + public VectorPassTest() { - private TestingHosts _hosts; + _hosts = new TestingHosts(); + } - public VectorPassTest() - { - _hosts = new TestingHosts(); - } + public void Dispose() + { + Overrides.SerializerOverride.Remove(typeof(Vector3)); + Overrides.CompressorOverride.Remove(typeof(Vector3)); + _hosts.Dispose(); + } - public void Dispose() - { - Overrides.SerializerOverride.Remove(typeof(Vector3)); - Overrides.CompressorOverride.Remove(typeof(Vector3)); - _hosts.Dispose(); - } + [Fact(Timeout = 1000)] + public void MsgPackPassVector3() + { + Overrides.SerializerOverride[typeof(Vector3)] = new MsgPackSerializer(); + Overrides.CompressorOverride.Remove(typeof(Vector3)); + PassVector3(); + } - [Fact(Timeout = 1000)] - public void MsgPackPassVector3() - { - Overrides.SerializerOverride[typeof(Vector3)] = new MsgPackSerializer(); - Overrides.CompressorOverride.Remove(typeof(Vector3)); - PassVector3(); - } + [Fact(Timeout = 1000)] + public void ReloadedPassVector3() + { + Overrides.SerializerOverride[typeof(Vector3)] = new ReloadedMemorySerializer(false); + Overrides.CompressorOverride.Remove(typeof(Vector3)); + PassVector3(); + } - [Fact(Timeout = 1000)] - public void ReloadedPassVector3() - { - Overrides.SerializerOverride[typeof(Vector3)] = new ReloadedMemorySerializer(false); - Overrides.CompressorOverride.Remove(typeof(Vector3)); - PassVector3(); - } + [Fact(Timeout = 1000)] + public void SystemTextJsonPassVector3() + { + Overrides.SerializerOverride[typeof(Vector3)] = new SystemTextJsonSerializer(new JsonSerializerOptions()); + Overrides.CompressorOverride.Remove(typeof(Vector3)); + PassVector3(); + } - [Fact(Timeout = 1000)] - public void SystemTextJsonPassVector3() - { - Overrides.SerializerOverride[typeof(Vector3)] = new SystemTextJsonSerializer(new JsonSerializerOptions()); - Overrides.CompressorOverride.Remove(typeof(Vector3)); - PassVector3(); - } + [Fact(Timeout = 1000)] + public void NewtonsoftPassVector3() + { + Overrides.SerializerOverride[typeof(Vector3)] = new NewtonsoftJsonSerializer(new JsonSerializerSettings()); + Overrides.CompressorOverride.Remove(typeof(Vector3)); + PassVector3(); + } - [Fact(Timeout = 1000)] - public void NewtonsoftPassVector3() - { - Overrides.SerializerOverride[typeof(Vector3)] = new NewtonsoftJsonSerializer(new JsonSerializerSettings()); - Overrides.CompressorOverride.Remove(typeof(Vector3)); - PassVector3(); - } + private void PassVector3() + { + Vector3 message = new Vector3(1.0F, 235.0F, 100.0F); + Vector3 delivered = default; - private void PassVector3() + // Message handling method + void Handler(ref NetMessage netMessage) { - Vector3 message = new Vector3(1.0F, 235.0F, 100.0F); - Vector3 delivered = default; - - // Message handling method - void Handler(ref NetMessage netMessage) - { - delivered = netMessage.Message; - } + delivered = netMessage.Message; + } - // Setup client - _hosts.SimpleClient.MessageHandler.AddOrOverrideHandler(Handler); + // Setup client + _hosts.SimpleClient.MessageHandler.AddOrOverrideHandler(Handler); - // Send Message. - var vectorMessage = new Message(message); - var data = vectorMessage.Serialize(); - _hosts.SimpleServer.NetManager.FirstPeer.Send(data, DeliveryMethod.ReliableOrdered); + // Send Message. + var vectorMessage = new Message(message); + var data = vectorMessage.Serialize(); + _hosts.SimpleServer.NetManager.FirstPeer.Send(data, DeliveryMethod.ReliableOrdered); - while (delivered.Equals(default(Vector3))) - Thread.Sleep(16); + while (delivered.Equals(default(Vector3))) + Thread.Sleep(16); - Assert.Equal(message, delivered); - } + Assert.Equal(message, delivered); } -} +} \ No newline at end of file From f716793047f138e3b90b1d3a999344c1bffea61a Mon Sep 17 00:00:00 2001 From: Sewer 56 Date: Wed, 6 Jul 2022 00:45:37 +0100 Subject: [PATCH 08/16] MessagePack: Fix Unused Contractless Resolver --- .../MsgPackSerializer.cs | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/Source/Reloaded.Messaging.Serializer.MessagePack/MsgPackSerializer.cs b/Source/Reloaded.Messaging.Serializer.MessagePack/MsgPackSerializer.cs index 19d9dfa..ce095db 100644 --- a/Source/Reloaded.Messaging.Serializer.MessagePack/MsgPackSerializer.cs +++ b/Source/Reloaded.Messaging.Serializer.MessagePack/MsgPackSerializer.cs @@ -9,12 +9,6 @@ namespace Reloaded.Messaging.Serializer.MessagePack; /// public class MsgPackSerializer : ISerializer { - /// - /// Any custom resolver to pass to MessagePack. - /// Default is - /// - public IFormatterResolver Resolver { get; private set; } = ContractlessStandardResolver.Instance; - /// /// Options for the MessagePack serializer. /// @@ -24,13 +18,11 @@ public class MsgPackSerializer : ISerializer /// Creates a new instance of the MessagePack serializer. /// /// - /// Custom resolver to pass to MessagePack, default is "Contractless Resolver" - /// (). + /// Custom resolver to pass to MessagePack, default instance uses "Contractless Resolver". /// public MsgPackSerializer(IFormatterResolver resolver = null) { - if (resolver != null) - Resolver = resolver; + SerializerOptions = SerializerOptions.WithResolver(resolver ?? ContractlessStandardResolver.Instance); } /// From 03432377ed1b176eb99291508161cfdb37ee9285 Mon Sep 17 00:00:00 2001 From: Sewer 56 Date: Wed, 6 Jul 2022 00:46:23 +0100 Subject: [PATCH 09/16] Added: Explicit NuGet Icon to ZStandard --- .../Reloaded.Messaging.Compressor.ZStandard.csproj | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Source/Reloaded.Messaging.Compressor.ZStandard/Reloaded.Messaging.Compressor.ZStandard.csproj b/Source/Reloaded.Messaging.Compressor.ZStandard/Reloaded.Messaging.Compressor.ZStandard.csproj index 6f8270b..eabcad3 100644 --- a/Source/Reloaded.Messaging.Compressor.ZStandard/Reloaded.Messaging.Compressor.ZStandard.csproj +++ b/Source/Reloaded.Messaging.Compressor.ZStandard/Reloaded.Messaging.Compressor.ZStandard.csproj @@ -8,11 +8,11 @@ Basic ZStandard compression implementation for Reloaded.Messaging based off of ZstdNet. Sewer56 https://github.com/Reloaded-Project/Reloaded.Messaging - https://avatars1.githubusercontent.com/u/45473408 https://github.com/Reloaded-Project/Reloaded.Messaging true 1.1.0 true + NuGet-Icon.png @@ -24,6 +24,10 @@ True + + True + \ + From a66d63c2ce27e96c4b2f6b9f75ab89bb482b29a7 Mon Sep 17 00:00:00 2001 From: Sewer 56 Date: Wed, 6 Jul 2022 00:47:48 +0100 Subject: [PATCH 10/16] Misc. Cleanup. --- Source/Reloaded.Messaging.Tests/Struct/Vector3.cs | 5 +---- Source/Reloaded.Messaging/MessageHandler.cs | 2 -- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/Source/Reloaded.Messaging.Tests/Struct/Vector3.cs b/Source/Reloaded.Messaging.Tests/Struct/Vector3.cs index 360e24d..4d9df8f 100644 --- a/Source/Reloaded.Messaging.Tests/Struct/Vector3.cs +++ b/Source/Reloaded.Messaging.Tests/Struct/Vector3.cs @@ -31,10 +31,7 @@ public override bool Equals(object obj) { if (ReferenceEquals(null, obj)) return false; - - if (ReferenceEquals(this, obj)) - return true; - + if (obj.GetType() != this.GetType()) return false; diff --git a/Source/Reloaded.Messaging/MessageHandler.cs b/Source/Reloaded.Messaging/MessageHandler.cs index a65bbf3..30ae7c5 100644 --- a/Source/Reloaded.Messaging/MessageHandler.cs +++ b/Source/Reloaded.Messaging/MessageHandler.cs @@ -27,9 +27,7 @@ public void Handle(ref RawNetMessage parameters) { var messageType = MessageBase.GetMessageType(parameters.Message); if (_mapping.TryGetValue(messageType, out RawNetMessageHandler value)) - { value(ref parameters); - } } /// From 40c0ac788853d93d46b45701bbc3e078945e89be Mon Sep 17 00:00:00 2001 From: Sewer 56 Date: Wed, 6 Jul 2022 00:50:19 +0100 Subject: [PATCH 11/16] Changed: Version to 2.X.X in all projects, backcompat be damned. --- .../Reloaded.Messaging.Compressor.ZStandard.csproj | 2 +- .../Reloaded.Messaging.Interfaces.csproj | 1 + .../Reloaded.Messaging.Serializer.MessagePack.csproj | 2 +- .../Reloaded.Messaging.Serializer.NewtonsoftJson.csproj | 3 ++- .../Reloaded.Messaging.Serializer.ReloadedMemory.csproj | 2 +- .../Reloaded.Messaging.Serializer.SystemTextJson.csproj | 2 +- 6 files changed, 7 insertions(+), 5 deletions(-) diff --git a/Source/Reloaded.Messaging.Compressor.ZStandard/Reloaded.Messaging.Compressor.ZStandard.csproj b/Source/Reloaded.Messaging.Compressor.ZStandard/Reloaded.Messaging.Compressor.ZStandard.csproj index eabcad3..5c469cb 100644 --- a/Source/Reloaded.Messaging.Compressor.ZStandard/Reloaded.Messaging.Compressor.ZStandard.csproj +++ b/Source/Reloaded.Messaging.Compressor.ZStandard/Reloaded.Messaging.Compressor.ZStandard.csproj @@ -10,7 +10,7 @@ https://github.com/Reloaded-Project/Reloaded.Messaging https://github.com/Reloaded-Project/Reloaded.Messaging true - 1.1.0 + 2.0.0 true NuGet-Icon.png diff --git a/Source/Reloaded.Messaging.Interfaces/Reloaded.Messaging.Interfaces.csproj b/Source/Reloaded.Messaging.Interfaces/Reloaded.Messaging.Interfaces.csproj index 93d15ae..43876b9 100644 --- a/Source/Reloaded.Messaging.Interfaces/Reloaded.Messaging.Interfaces.csproj +++ b/Source/Reloaded.Messaging.Interfaces/Reloaded.Messaging.Interfaces.csproj @@ -11,6 +11,7 @@ This package exists to allow you to use various features of the library, such as https://github.com/Reloaded-Project/Reloaded.Messaging true NuGet-Icon.png + 2.0.0 diff --git a/Source/Reloaded.Messaging.Serializer.MessagePack/Reloaded.Messaging.Serializer.MessagePack.csproj b/Source/Reloaded.Messaging.Serializer.MessagePack/Reloaded.Messaging.Serializer.MessagePack.csproj index 31b6e83..8b1a02d 100644 --- a/Source/Reloaded.Messaging.Serializer.MessagePack/Reloaded.Messaging.Serializer.MessagePack.csproj +++ b/Source/Reloaded.Messaging.Serializer.MessagePack/Reloaded.Messaging.Serializer.MessagePack.csproj @@ -12,7 +12,7 @@ https://github.com/Reloaded-Project/Reloaded.Messaging https://github.com/Reloaded-Project/Reloaded.Messaging true - 1.1.0 + 2.0.0 true NuGet-Icon.png diff --git a/Source/Reloaded.Messaging.Serializer.NewtonsoftJson/Reloaded.Messaging.Serializer.NewtonsoftJson.csproj b/Source/Reloaded.Messaging.Serializer.NewtonsoftJson/Reloaded.Messaging.Serializer.NewtonsoftJson.csproj index 4331967..d8a3cf6 100644 --- a/Source/Reloaded.Messaging.Serializer.NewtonsoftJson/Reloaded.Messaging.Serializer.NewtonsoftJson.csproj +++ b/Source/Reloaded.Messaging.Serializer.NewtonsoftJson/Reloaded.Messaging.Serializer.NewtonsoftJson.csproj @@ -13,7 +13,8 @@ https://github.com/Reloaded-Project/Reloaded.Messaging true true - 1.0.1 + + 2.0.0 NuGet-Icon.png diff --git a/Source/Reloaded.Messaging.Serializer.ReloadedMemory/Reloaded.Messaging.Serializer.ReloadedMemory.csproj b/Source/Reloaded.Messaging.Serializer.ReloadedMemory/Reloaded.Messaging.Serializer.ReloadedMemory.csproj index 84f8239..3a1f75b 100644 --- a/Source/Reloaded.Messaging.Serializer.ReloadedMemory/Reloaded.Messaging.Serializer.ReloadedMemory.csproj +++ b/Source/Reloaded.Messaging.Serializer.ReloadedMemory/Reloaded.Messaging.Serializer.ReloadedMemory.csproj @@ -10,7 +10,7 @@ https://github.com/Reloaded-Project/Reloaded.Messaging https://github.com/Reloaded-Project/Reloaded.Messaging true - 1.1.0 + 2.0.0 true NuGet-Icon.png diff --git a/Source/Reloaded.Messaging.Serializer.SystemTextJson/Reloaded.Messaging.Serializer.SystemTextJson.csproj b/Source/Reloaded.Messaging.Serializer.SystemTextJson/Reloaded.Messaging.Serializer.SystemTextJson.csproj index fdbae84..1616a55 100644 --- a/Source/Reloaded.Messaging.Serializer.SystemTextJson/Reloaded.Messaging.Serializer.SystemTextJson.csproj +++ b/Source/Reloaded.Messaging.Serializer.SystemTextJson/Reloaded.Messaging.Serializer.SystemTextJson.csproj @@ -13,7 +13,7 @@ https://github.com/Reloaded-Project/Reloaded.Messaging true true - 1.0.2 + 2.0.0 NuGet-Icon.png From 89787e33de13b810eace57897a1221bda0cdf5a5 Mon Sep 17 00:00:00 2001 From: Sewer 56 Date: Fri, 8 Jul 2022 23:34:50 +0100 Subject: [PATCH 12/16] Version 2.0.0: Chaos and Invade --- Docs/ImplementingCompressorsSerializers.md | 81 ---------- Docs/UseAsNetworkingLibrary.md | 136 ----------------- Docs/UseAsSerializationLibrary.md | 47 ------ README.md | 48 +++--- .../DeserializationBenchmark.cs | 97 ++++++++++++ .../MessageHandlerBenchmark.cs | 79 ++++++++++ .../MessagePackingRealScenarioBenchmark.cs | 109 ++++++++++++++ .../PackOverheadBenchmark.cs | 112 ++++++++++++++ .../Reloaded.Messaging.Benchmarks/Program.cs | 56 +++++++ .../Reloaded.Messaging.Benchmarks.csproj | 24 +++ .../SerializationBenchmark.cs | 73 +++++++++ .../Structures/ModConfig.cs | 104 +++++++++++++ .../Utilities/ArrayExtensions.cs | 14 ++ .../Utilities/Constants.cs | 12 ++ .../Utilities/DummyCompressor.cs | 29 ++++ .../Utilities/DummyMessageHandlers.cs | 21 +++ .../Utilities/DummySerializer.cs | 16 ++ .../Utilities/FakerExtensions.cs | 28 ++++ .../Utilities/SpanExtensions.cs | 28 ++++ ...aded.Messaging.Compressor.ZStandard.csproj | 2 +- .../ZStandardCompressor.cs | 24 ++- .../BrotliCompressor.cs | 53 +++++++ .../Reloaded.Messaging.Extras.Runtime/Pool.cs | 22 +++ .../Reloaded.Messaging.Extras.Runtime.csproj} | 20 +-- ...SourceGeneratedSystemTextJsonSerializer.cs | 45 ++++++ .../SystemTextJsonSerializer.cs | 54 +++++++ .../LiteNetLibHost.cs | 138 ++++++++++++++++++ .../LiteNetLibState.cs | 46 ++++++ ...Reloaded.Messaging.Host.LiteNetLib.csproj} | 36 +++-- .../ICompressor.cs | 22 ++- Source/Reloaded.Messaging.Interfaces/IHost.cs | 25 ++++ .../Reloaded.Messaging.Interfaces/IMessage.cs | 6 +- .../IMessageDispatcher.cs | 34 +++++ .../IMessageHandlerBase.cs | 23 +++ .../ISerializer.cs | 13 +- .../Message/ISerializable.cs | 7 +- .../Reloaded.Messaging.Interfaces.csproj | 9 +- .../Serializable.cs | 53 ------- .../Utilities/NullCompressor.cs | 29 ++++ .../MessagePackSerializer.cs | 60 ++++++++ .../MsgPackSerializer.cs | 40 ----- ...ed.Messaging.Serializer.MessagePack.csproj | 5 +- .../UnmanagedMemoryManager.cs | 52 +++++++ .../NewtonsoftJsonSerializer.cs | 39 ----- ...Messaging.Serializer.ReloadedMemory.csproj | 7 +- .../ReloadedMemorySerializer.cs | 42 ------ .../UnmanagedReloadedMemorySerializer.cs | 37 +++++ .../SystemTextJsonSerializer.cs | 35 ----- .../Init/TestingHosts.cs | 32 ---- .../LiteNetLibHostTests.cs | 72 +++++++++ .../MessageCreationTests.cs | 133 +++++++++++++++++ .../MessageDispatcherTests.cs | 88 +++++++++++ .../Reloaded.Messaging.Tests/MessageType.cs | 7 - .../Messages/MessageType.cs | 11 ++ .../{Struct => Messages}/Vector3.cs | 28 ++-- .../Messages/Vector3Brotli.cs | 53 +++++++ .../Vector3MessagePackNoCompression.cs | 52 +++++++ .../Vector3ReloadedMemoryDummyCompression.cs | 52 +++++++ .../Messages/Vector3SystemTextJson.cs | 52 +++++++ .../Vector3SystemTextJsonSourceGenerated.cs | 61 ++++++++ .../Messages/Vector3ZStandard.cs | 52 +++++++ .../Reloaded.Messaging.Tests.csproj | 8 +- .../Struct/StringMessage.cs | 36 ----- .../Tests/Compression/CompressionTest.cs | 45 ------ .../Tests/Serialization/PureSerialize.cs | 23 --- .../Tests/Serialization/StringPassTest.cs | 91 ------------ .../Tests/Serialization/VectorPassTest.cs | 90 ------------ Source/Reloaded.Messaging.sln | 27 +++- .../Reloaded.Messaging/MessageDispatcher.cs | 67 +++++++++ Source/Reloaded.Messaging/MessageHandler.cs | 68 --------- .../ReusableSingletonMemoryStream.cs | 27 ++++ .../Messages/HeaderReader.cs | 59 ++++++++ .../Messages/IMessageExtensions.cs | 19 --- Source/Reloaded.Messaging/Messages/Message.cs | 121 --------------- .../Messages/MessageBase.cs | 21 --- .../Messages/MessageReader.cs | 51 +++++++ .../Messages/MessageWriter.cs | 121 +++++++++++++++ Source/Reloaded.Messaging/Overrides.cs | 22 --- .../ReferenceMessageHandler.cs | 70 +++++++++ .../Reloaded.Messaging.csproj | 13 +- Source/Reloaded.Messaging/SimpleHost.cs | 84 ----------- .../Reloaded.Messaging/Structs/NetMessage.cs | 42 ------ .../Structs/RawNetMessage.cs | 44 ------ .../Utilities/ArrayRental.cs | 65 +++++++++ .../Utilities/IMessageExtensions.cs | 61 ++++++++ Source/Reloaded.Messaging/Utilities/Pool.cs | 39 +++++ .../Utilities/SpanExtensions.cs | 52 +++++++ docs/benchmarks.md | 77 ++++++++++ docs/defining-structures.md | 72 +++++++++ docs/images/reloaded-icon.png | Bin 0 -> 47501 bytes docs/index.md | 32 ++++ docs/message-structure.md | 50 +++++++ docs/running-host.md | 69 +++++++++ docs/serializers-compressors.md | 111 ++++++++++++++ mkdocs.yml | 50 +++++++ 95 files changed, 3268 insertions(+), 1344 deletions(-) delete mode 100644 Docs/ImplementingCompressorsSerializers.md delete mode 100644 Docs/UseAsNetworkingLibrary.md delete mode 100644 Docs/UseAsSerializationLibrary.md create mode 100644 Source/Reloaded.Messaging.Benchmarks/DeserializationBenchmark.cs create mode 100644 Source/Reloaded.Messaging.Benchmarks/MessageHandlerBenchmark.cs create mode 100644 Source/Reloaded.Messaging.Benchmarks/MessagePackingRealScenarioBenchmark.cs create mode 100644 Source/Reloaded.Messaging.Benchmarks/PackOverheadBenchmark.cs create mode 100644 Source/Reloaded.Messaging.Benchmarks/Program.cs create mode 100644 Source/Reloaded.Messaging.Benchmarks/Reloaded.Messaging.Benchmarks.csproj create mode 100644 Source/Reloaded.Messaging.Benchmarks/SerializationBenchmark.cs create mode 100644 Source/Reloaded.Messaging.Benchmarks/Structures/ModConfig.cs create mode 100644 Source/Reloaded.Messaging.Benchmarks/Utilities/ArrayExtensions.cs create mode 100644 Source/Reloaded.Messaging.Benchmarks/Utilities/Constants.cs create mode 100644 Source/Reloaded.Messaging.Benchmarks/Utilities/DummyCompressor.cs create mode 100644 Source/Reloaded.Messaging.Benchmarks/Utilities/DummyMessageHandlers.cs create mode 100644 Source/Reloaded.Messaging.Benchmarks/Utilities/DummySerializer.cs create mode 100644 Source/Reloaded.Messaging.Benchmarks/Utilities/FakerExtensions.cs create mode 100644 Source/Reloaded.Messaging.Benchmarks/Utilities/SpanExtensions.cs create mode 100644 Source/Reloaded.Messaging.Extras.Runtime/BrotliCompressor.cs create mode 100644 Source/Reloaded.Messaging.Extras.Runtime/Pool.cs rename Source/{Reloaded.Messaging.Serializer.NewtonsoftJson/Reloaded.Messaging.Serializer.NewtonsoftJson.csproj => Reloaded.Messaging.Extras.Runtime/Reloaded.Messaging.Extras.Runtime.csproj} (75%) create mode 100644 Source/Reloaded.Messaging.Extras.Runtime/SourceGeneratedSystemTextJsonSerializer.cs create mode 100644 Source/Reloaded.Messaging.Extras.Runtime/SystemTextJsonSerializer.cs create mode 100644 Source/Reloaded.Messaging.Host.LiteNetLib/LiteNetLibHost.cs create mode 100644 Source/Reloaded.Messaging.Host.LiteNetLib/LiteNetLibState.cs rename Source/{Reloaded.Messaging.Serializer.SystemTextJson/Reloaded.Messaging.Serializer.SystemTextJson.csproj => Reloaded.Messaging.Host.LiteNetLib/Reloaded.Messaging.Host.LiteNetLib.csproj} (67%) create mode 100644 Source/Reloaded.Messaging.Interfaces/IHost.cs create mode 100644 Source/Reloaded.Messaging.Interfaces/IMessageDispatcher.cs create mode 100644 Source/Reloaded.Messaging.Interfaces/IMessageHandlerBase.cs delete mode 100644 Source/Reloaded.Messaging.Interfaces/Serializable.cs create mode 100644 Source/Reloaded.Messaging.Interfaces/Utilities/NullCompressor.cs create mode 100644 Source/Reloaded.Messaging.Serializer.MessagePack/MessagePackSerializer.cs delete mode 100644 Source/Reloaded.Messaging.Serializer.MessagePack/MsgPackSerializer.cs create mode 100644 Source/Reloaded.Messaging.Serializer.MessagePack/UnmanagedMemoryManager.cs delete mode 100644 Source/Reloaded.Messaging.Serializer.NewtonsoftJson/NewtonsoftJsonSerializer.cs delete mode 100644 Source/Reloaded.Messaging.Serializer.ReloadedMemory/ReloadedMemorySerializer.cs create mode 100644 Source/Reloaded.Messaging.Serializer.ReloadedMemory/UnmanagedReloadedMemorySerializer.cs delete mode 100644 Source/Reloaded.Messaging.Serializer.SystemTextJson/SystemTextJsonSerializer.cs delete mode 100644 Source/Reloaded.Messaging.Tests/Init/TestingHosts.cs create mode 100644 Source/Reloaded.Messaging.Tests/LiteNetLibHostTests.cs create mode 100644 Source/Reloaded.Messaging.Tests/MessageCreationTests.cs create mode 100644 Source/Reloaded.Messaging.Tests/MessageDispatcherTests.cs delete mode 100644 Source/Reloaded.Messaging.Tests/MessageType.cs create mode 100644 Source/Reloaded.Messaging.Tests/Messages/MessageType.cs rename Source/Reloaded.Messaging.Tests/{Struct => Messages}/Vector3.cs (52%) create mode 100644 Source/Reloaded.Messaging.Tests/Messages/Vector3Brotli.cs create mode 100644 Source/Reloaded.Messaging.Tests/Messages/Vector3MessagePackNoCompression.cs create mode 100644 Source/Reloaded.Messaging.Tests/Messages/Vector3ReloadedMemoryDummyCompression.cs create mode 100644 Source/Reloaded.Messaging.Tests/Messages/Vector3SystemTextJson.cs create mode 100644 Source/Reloaded.Messaging.Tests/Messages/Vector3SystemTextJsonSourceGenerated.cs create mode 100644 Source/Reloaded.Messaging.Tests/Messages/Vector3ZStandard.cs delete mode 100644 Source/Reloaded.Messaging.Tests/Struct/StringMessage.cs delete mode 100644 Source/Reloaded.Messaging.Tests/Tests/Compression/CompressionTest.cs delete mode 100644 Source/Reloaded.Messaging.Tests/Tests/Serialization/PureSerialize.cs delete mode 100644 Source/Reloaded.Messaging.Tests/Tests/Serialization/StringPassTest.cs delete mode 100644 Source/Reloaded.Messaging.Tests/Tests/Serialization/VectorPassTest.cs create mode 100644 Source/Reloaded.Messaging/MessageDispatcher.cs delete mode 100644 Source/Reloaded.Messaging/MessageHandler.cs create mode 100644 Source/Reloaded.Messaging/Messages/Disposables/ReusableSingletonMemoryStream.cs create mode 100644 Source/Reloaded.Messaging/Messages/HeaderReader.cs delete mode 100644 Source/Reloaded.Messaging/Messages/IMessageExtensions.cs delete mode 100644 Source/Reloaded.Messaging/Messages/Message.cs delete mode 100644 Source/Reloaded.Messaging/Messages/MessageBase.cs create mode 100644 Source/Reloaded.Messaging/Messages/MessageReader.cs create mode 100644 Source/Reloaded.Messaging/Messages/MessageWriter.cs delete mode 100644 Source/Reloaded.Messaging/Overrides.cs create mode 100644 Source/Reloaded.Messaging/ReferenceMessageHandler.cs delete mode 100644 Source/Reloaded.Messaging/SimpleHost.cs delete mode 100644 Source/Reloaded.Messaging/Structs/NetMessage.cs delete mode 100644 Source/Reloaded.Messaging/Structs/RawNetMessage.cs create mode 100644 Source/Reloaded.Messaging/Utilities/ArrayRental.cs create mode 100644 Source/Reloaded.Messaging/Utilities/IMessageExtensions.cs create mode 100644 Source/Reloaded.Messaging/Utilities/Pool.cs create mode 100644 Source/Reloaded.Messaging/Utilities/SpanExtensions.cs create mode 100644 docs/benchmarks.md create mode 100644 docs/defining-structures.md create mode 100644 docs/images/reloaded-icon.png create mode 100644 docs/index.md create mode 100644 docs/message-structure.md create mode 100644 docs/running-host.md create mode 100644 docs/serializers-compressors.md create mode 100644 mkdocs.yml diff --git a/Docs/ImplementingCompressorsSerializers.md b/Docs/ImplementingCompressorsSerializers.md deleted file mode 100644 index f1a2e76..0000000 --- a/Docs/ImplementingCompressorsSerializers.md +++ /dev/null @@ -1,81 +0,0 @@ -## Implementing Compressors & Serializers -Reloaded.Messaging allows you to specify your own serializers and compressors responsible for the serialization of the message to be transmitted. - -Adding support for these is very trivial. - -### Serializers - -To implement a serializer, simply make a class that implements the `ISerializer` interface. - -```csharp -public interface ISerializer -{ - TStruct Deserialize(byte[] serialized); - byte[] Serialize(ref TStruct item); -} -``` - -A simple example implementation using [MessagePack-CSharp](https://github.com/neuecc/MessagePack-CSharp) could look like this: - -```csharp -public class MsgPackSerializer : ISerializer -{ - public TStruct Deserialize(byte[] serialized) => MessagePackSerializer.Deserialize(serialized); - - public byte[] Serialize(ref TStruct item) => MessagePackSerializer.Serialize(item); -} -``` - -There is no default serializer, however one must be specified. -All serializers can be found in the `Reloaded.Messaging.Serializer` namespace, with serializers available as their own NuGet packages. - -### Compressors - -Implementing Compressors is virtually identical to implementing a Serializer. - -```csharp -public interface ICompressor -{ - byte[] Compress(byte[] data); - byte[] Decompress(byte[] data); -} -``` - -A simple example using [ZstdNet](https://github.com/skbkontur/ZstdNet) could look like this: - -```csharp -public class ZStandardCompressor : ICompressor, IDisposable -{ - public readonly ZstdNet.Compressor Compressor; - public readonly Decompressor Decompressor; - - public ZStandardCompressor(CompressionOptions compressionOptions = null, DecompressionOptions decompressionOptions = null) - { - Compressor = compressionOptions != null ? new ZstdNet.Compressor(compressionOptions) : new ZstdNet.Compressor(); - - Decompressor = decompressionOptions != null ? new Decompressor(decompressionOptions) : new Decompressor(); - } - - // Disposal - ~ZStandardCompressor() - { - Dispose(); - } - - public void Dispose() - { - Compressor?.Dispose(); - Decompressor?.Dispose(); - GC.SuppressFinalize(this); - } - - // ICompressor - public byte[] Compress(byte[] data) => Compressor.Wrap(data); - public byte[] Decompress(byte[] data) => Decompressor.Unwrap(data); -} -``` - -All compressors can be found in the `Reloaded.Messaging.Compressor` namespace, with compressors available as separate NuGet packages. - -If `null` is specified for the compressor, no compression will be performed. - diff --git a/Docs/UseAsNetworkingLibrary.md b/Docs/UseAsNetworkingLibrary.md deleted file mode 100644 index 62c1013..0000000 --- a/Docs/UseAsNetworkingLibrary.md +++ /dev/null @@ -1,136 +0,0 @@ -# Table of Contents -- [Usage (As Networking Library)](#usage-as-networking-library) - - [How It Works (Summary)](#how-it-works-summary) - - [A: Unique Message Identifier](#a-unique-message-identifier) - - [B: Implementing IMessage\](#b-implementing-imessagemessagetype) - - [C. Connecting Host & Client](#c-connecting-host--client) - - [D. Receiving Messages](#d-receiving-messages) - - [E. Send Messages](#e-send-messages) -- [Custom Compressors & Serializers](#custom-compressors--serializers) -- [Other Information](#other-information) - - [Overriding Serializers & Compressors at Runtime](#overriding-serializers--compressors-at-runtime) - - [Default Settings (LiteNetLib)](#default-settings-litenetlib) - - [Connection Requests](#connection-requests) - - [Packet Handling](#packet-handling) - -## Usage (As Networking Library) - -### How It Works (Summary) -A. User specifies or chooses an individual unmanaged type `TMessageType` (recommend enum), where each unique value corresponds to different message structure. - -B. User implements interface `IMessage` in types they want to send over the network. - -C. User creates `SimpleHost` instance(s) for Server/Client with type from `A` as generic type. - -D. User registers methods to handle different values for `TMessageType`. - -And then message sending/receiving can proceed. - -Note: A complete working example can be found in the basic test collection, `Reloaded.Messaging.Tests`. - -### A: Unique Message Identifier -A unique message identifier `TMessageType` can be any unmanaged type. -The recommended type is enum. - -```csharp -// We have less than 256 values, so use byte. -// Default for enum is int, but we don't need extra 3 bytes of overhead in every message. -public enum MessageType : byte -{ - String, - Vector3 -} -``` - -### B: Implementing IMessage\ -The interface specifies the compressor and serializer used to pack the specified structure. -It acts as a contract and therefore should match between the server and client. - -No information about the serializer or compressor is sent with any of the packets as that would incur unnecessary additional overhead. - -```csharp -public struct Vector3 : IMessage -{ - // IMessage - public MessageType GetMessageType() => MessageType.Vector3; - public ISerializer GetSerializer() => new MsgPackSerializer(true); - public ICompressor GetCompressor() => null; - - // Members - public float X { get; set; } - public float Y { get; set; } - public float Z { get; set; } -} -``` - -A serializer must be specified. Compressor is optional. - -### C. Connecting Host & Client - -The following example creates a new server and client on the local machine and connects them to each other. - -```csharp -// DefaultPassword = "RandomString" -// MessageType = enum (byte) (see above) -SimpleServer = new SimpleHost(true, DefaultPassword); -SimpleClient = new SimpleHost(false, DefaultPassword); - -SimpleServer.NetManager.Start(IPAddress.Loopback, IPAddress.IPv6Loopback, 0); -SimpleClient.NetManager.Start(IPAddress.Loopback, IPAddress.IPv6Loopback, 0); -SimpleClient.NetManager.Connect(new IPEndPoint(IPAddress.Loopback, SimpleServer.NetManager.LocalPort), DefaultPassword); -``` - -### D. Receiving Messages -```csharp -// Register a function "Handler" to deal with incoming messages of type Vector3. (MessageType is obtained from IMessage Interface) -SimpleClient.MessageHandler.AddOrOverrideHandler(Handler); - -static void Handler(ref NetMessage netMessage) -{ - var vector3 = netMessage.Message; - // Do something with Vector3 -} -``` - -### E. Send Messages -To send a message, create an instance of `Message`, where `TStruct` is a struct from `B` that inherits `IMessage`. - -```csharp -var vectorMessage = new Message(message); // Wraps the message for sending. -byte[] data = vectorMessage.Serialize(); // Serializes and compresses using Serializer/Compressor defined in IMessage implementation. - -SimpleServer.NetManager.FirstPeer.Send(data, DeliveryMethod.ReliableOrdered); // Regular LiteNetLib usage. -``` - -## Custom Compressors & Serializers -See [ImplementingCompressorsSerializers.md](./ImplementingCompressorsSerializers.md) - -## Other Information - -### Overriding Serializers & Compressors at Runtime -*Note: This feature is mainly intended for benchmarking and testing.* -*Changes are client-side (program-side) only and not broadcasted to clients etc.* - -It is possible to override the compressor and/or serializer used to handle a specific type at runtime. - -Usage Examples: -```csharp -// Override Vector3 -Overrides.SerializerOverride[typeof(Vector3)] = new MsgPackSerializer(false); -Overrides.CompressorOverride[typeof(Vector3)] = null; - -// Remove overrides for Vector3 -Overrides.SerializerOverride.Remove(typeof(Vector3)); -Overrides.CompressorOverride.Remove(typeof(Vector3)); -``` - -### Default Settings (LiteNetLib) -`SimpleHost` uses the following default settings for the LiteNetLib library. - -#### Connection Requests -- Subscribes to `ConnectionRequestEvent`, allowing clients to connect only with password set in constructor. (Which can be changed after instantiation) - -#### Packet Handling -- Sets `UnsyncedEvents` to true. Messages are received automatically on background thread. -- Sets `AutoRecycle` to true. Automatically recycling NetPacketReader. -- Subscribes to `NetworkReceiveEvent`, to automatically handle incoming packets that have been assigned to the `MessageHandler`. \ No newline at end of file diff --git a/Docs/UseAsSerializationLibrary.md b/Docs/UseAsSerializationLibrary.md deleted file mode 100644 index 5144c54..0000000 --- a/Docs/UseAsSerializationLibrary.md +++ /dev/null @@ -1,47 +0,0 @@ -## Usage (As Serialization Library) - -### Define Serializable Struct/Class - -In order to enable serialization for your class, you should implement the `ISerializable` interface. - -```csharp -public struct Vector3 : ISerializable -{ - // ISerializable - public ISerializer GetSerializer() => ReloadedMemorySerializer(false); - public ICompressor GetCompressor() => null; - - // Members - public float X { get; set; } - public float Y { get; set; } - public float Z { get; set; } -} -``` - -Returning an `ISerializer` is required. Returning an `ICompressor` is optional. - -### Serialize - -To serialize an instance, call the extension method `Serializable.Serialize()` in `Reloaded.Messaging.Interfaces.Serializable`. - -```csharp -var vector = new Vector3(0, 25, 100); -byte[] data = vector.Serialize(); -``` - -### Deserialize - -To serialize an instance, call the extension method `Serializable.Deserialize()` in `Reloaded.Messaging.Interfaces.Serializable`. - -```csharp -byte[] data = vector.Serialize(); -var newVector = Serializable.Deserialize(data); -``` - -### Custom Compressors & Serializers -See [ImplementingCompressorsSerializers.md](./ImplementingCompressorsSerializers.md) - -### Other Notes - -- It is possible to use serializer specific attributes/markup etc. for your struct members. -- Some serializers may require the use of properties for serialization/deserialization. \ No newline at end of file diff --git a/README.md b/README.md index 92a90d0..517d9da 100644 --- a/README.md +++ b/README.md @@ -6,42 +6,28 @@

-# Packages -**Reloaded.Messaging:** NuGet - -**Reloaded.Messaging.Interfaces:** NuGet - -**Reloaded.Messaging.Serializer.MessagePack**: NuGet - -**Reloaded.Messaging.Serializer.ReloadedMemory**: NuGet - -**Reloaded.Messaging.Serializer.SystemTextJson**: NuGet - -**Reloaded.Messaging.Serializer.NewtonsoftJson**: NuGet - -**Reloaded.Messaging.Compressor.ZStandard**: NuGet - # Introduction -Reloaded.Networking is [Reloaded II](https://github.com/Reloaded-Project/Reloaded-II/)'s Networking and Serialization library. The main goal for the library is to provide an extensible "event-like" solution for passing messages across a local or remote network that extends on the base functionality of [LiteNetLib](https://github.com/RevenantX/LiteNetLib) by Ruslan Pyrch (RevenantX) . - -It has been slightly extended in the hope of becoming more general purpose, perhaps to be reused in other projects. -## Idea -`Reloaded.Networking` is a simple barebones library to solve a deceptively annoying problem: Writing code that distinguishes the type of message received over a network and performs a specific action. +Reloaded.Networking is library that adds support for simple, high performance message packing to existing networking libraries. -## Characteristics -- Minimal networking overhead in most use cases (1 byte)*. -- Choice of serializer/compressor on a per type (struct/class) basis. -- Simple to use. +Specifically, it provides a minimal framework for performing the following tasks: -*Assuming user has less than 256 unique types of network messages. +- Asynchronous message processing for external networking libraries. +- Sending/Receiving messages with (de)serialization and [optional] (de)compression. +- Automatically dispatching messages to appropriate handlers (per message type). -*Alternative unmanaged types (e.g. short, int) can be specified increasing overhead to `sizeof(type)` and respectively increasing max unique types.* +It was originally created for Reloaded II, however has been extended in the hope of becoming a more general purpose library. + +This library is heavily optimized for achieving high throughput for messages `< 128KB`. -## Usage - -[Usage: As Networking Library](./Docs/UseAsNetworkingLibrary.md) +## Characteristics +- High performance. (Memory pooling, low heap allocation, stack allocation). +- Low networking overhead. +- Custom serializer/compressor per class type. +- Simple message packing/protocol. +- 1 byte overhead for uncompressed, 5 bytes for compressed. +- Unsafe. -[Usage: As Serialization Library](./Docs/UseAsSerializationLibrary.md) +## Documentation -[Adding 3rd Party Compressors & Serializers](./Docs/ImplementingCompressorsSerializers.md) +More information can be found in the dedicated documentation site. \ No newline at end of file diff --git a/Source/Reloaded.Messaging.Benchmarks/DeserializationBenchmark.cs b/Source/Reloaded.Messaging.Benchmarks/DeserializationBenchmark.cs new file mode 100644 index 0000000..6436109 --- /dev/null +++ b/Source/Reloaded.Messaging.Benchmarks/DeserializationBenchmark.cs @@ -0,0 +1,97 @@ +using System.Runtime; +using System.Runtime.InteropServices; +using BenchmarkDotNet.Attributes; +using FastSerialization; +using Microsoft.IO; +using Reloaded.Messaging.Benchmarks.Structures; +using Reloaded.Messaging.Benchmarks.Utilities; +using Reloaded.Messaging.Extras.Runtime; +using Reloaded.Messaging.Interfaces; +using Reloaded.Messaging.Serializer.MessagePack; + +namespace Reloaded.Messaging.Benchmarks; + +[MemoryDiagnoser] +public class DeserializationBenchmark +{ + public const int UnrollFactor = 5; + + [Params(Constants.DefaultOperationCount)] + public int NumItems { get; set; } + + private RecyclableMemoryStreamManager _streamManager = new(); + private RecyclableMemoryStream _stream; + private byte[][] _jsons = null!; + private byte[][] _msgPacks = null!; + + private SourceGeneratedSystemTextJsonSerializer _srcGenSystemTextJsonSerializer; + private SystemTextJsonSerializer _systemTextJsonSerializer = new(); + private MessagePackSerializer _messagePackSerializer = new(); + + [GlobalSetup] + public void Setup() + { + _srcGenSystemTextJsonSerializer = new(ModConfigContext.Default.ModConfig); + + // Prepare the data by serializing first. + using (var serializeStream = (RecyclableMemoryStream)_streamManager.GetStream()) + { + var items = ModConfig.Create(NumItems); + _jsons = new byte[NumItems][]; + _msgPacks = new byte[NumItems][]; + + for (int x = 0; x < items.Length; x++) + { +#pragma warning disable CS0618 + serializeStream.SetLength(0); + _srcGenSystemTextJsonSerializer.Serialize(ref items.GetWithoutBoundsChecks(x), serializeStream); + _jsons[x] = serializeStream.ToArray(); + + serializeStream.SetLength(0); + _messagePackSerializer.Serialize(ref items.GetWithoutBoundsChecks(x), serializeStream); + _msgPacks[x] = serializeStream.ToArray(); +#pragma warning restore CS0618 + } + } + + _stream = (RecyclableMemoryStream)_streamManager.GetStream(); + GC.Collect(); + } + + [GlobalCleanup] + public void Cleanup() + { + _jsons = null!; + _msgPacks = null!; + GC.Collect(); + } + + [IterationCleanup] + public void IterationSetup() + { + _stream.Seek(0, SeekOrigin.Begin); + } + + [Benchmark] + public void SystemTextJson() => BenchmarkCommon(_jsons, _systemTextJsonSerializer); + + [Benchmark] + public void SystemTextJsonSrcGen() => BenchmarkCommon(_jsons, _srcGenSystemTextJsonSerializer); + + [Benchmark] + public void MessagePack() => BenchmarkCommon(_msgPacks, _messagePackSerializer); + + private void BenchmarkCommon(byte[][] items, TSerializer serializer) where TSerializer : ISerializer + { + var numIterations = _jsons.Length / UnrollFactor; + for (int x = 0; x < numIterations; x++) + { + var baseIndex = x * UnrollFactor; + serializer.Deserialize(items.GetWithoutBoundsChecks(baseIndex + 0).AsSpanFast()); + serializer.Deserialize(items.GetWithoutBoundsChecks(baseIndex + 1).AsSpanFast()); + serializer.Deserialize(items.GetWithoutBoundsChecks(baseIndex + 2).AsSpanFast()); + serializer.Deserialize(items.GetWithoutBoundsChecks(baseIndex + 3).AsSpanFast()); + serializer.Deserialize(items.GetWithoutBoundsChecks(baseIndex + 4).AsSpanFast()); + } + } +} \ No newline at end of file diff --git a/Source/Reloaded.Messaging.Benchmarks/MessageHandlerBenchmark.cs b/Source/Reloaded.Messaging.Benchmarks/MessageHandlerBenchmark.cs new file mode 100644 index 0000000..bc28613 --- /dev/null +++ b/Source/Reloaded.Messaging.Benchmarks/MessageHandlerBenchmark.cs @@ -0,0 +1,79 @@ +using BenchmarkDotNet.Attributes; +using FastSerialization; +using Microsoft.IO; +using Reloaded.Messaging.Benchmarks.Structures; +using Reloaded.Messaging.Benchmarks.Utilities; +using Reloaded.Messaging.Extras.Runtime; +using Reloaded.Messaging.Interfaces.Utilities; + +namespace Reloaded.Messaging.Benchmarks; + +[MemoryDiagnoser] +public class MessageHandlerBenchmark +{ + private MessageDispatcher _dispatcher; + private byte[][] _jsons = null!; + + [Params(Constants.DefaultOperationCount)] + public int NumItems { get; set; } + + private const int UnrollFactor = 10; + + [GlobalSetup] + public void Setup() + { + // Prepare the data by serializing first. + var streamManager = new RecyclableMemoryStreamManager(); + using (var serializeStream = (RecyclableMemoryStream)streamManager.GetStream()) + { + var serializer = new SourceGeneratedSystemTextJsonSerializer(ModConfigMessageContext.Default.ModConfigMessage); + var items = ModConfig.Create(NumItems); + _jsons = new byte[NumItems][]; + + for (int x = 0; x < items.Length; x++) + { +#pragma warning disable CS0618 + serializeStream.SetLength(0); + serializer.Serialize(ref items.GetWithoutBoundsChecks(x), serializeStream); + _jsons[x] = serializeStream.ToArray(); +#pragma warning restore CS0618 + } + } + + _dispatcher = new MessageDispatcher(); + _dispatcher.AddOrOverrideHandler(new DummyMessageHandlerNoDeserialize, NullCompressor>()); + GC.Collect(); + } + + [GlobalCleanup] + public void Cleanup() + { + _jsons = null; + GC.Collect(); + } + + + [Benchmark(Baseline = true)] + public void HandleMessage() + { + // Add handler. + var dispatcher = _dispatcher; + var numIterations = _jsons.Length / UnrollFactor; + var extraData = 0; + + for (int x = 0; x < numIterations; x++) + { + var baseIndex = x * UnrollFactor; + dispatcher.Dispatch(_jsons.GetWithoutBoundsChecks(baseIndex + 0).AsSpanFast(), ref extraData); + dispatcher.Dispatch(_jsons.GetWithoutBoundsChecks(baseIndex + 1).AsSpanFast(), ref extraData); + dispatcher.Dispatch(_jsons.GetWithoutBoundsChecks(baseIndex + 2).AsSpanFast(), ref extraData); + dispatcher.Dispatch(_jsons.GetWithoutBoundsChecks(baseIndex + 3).AsSpanFast(), ref extraData); + dispatcher.Dispatch(_jsons.GetWithoutBoundsChecks(baseIndex + 4).AsSpanFast(), ref extraData); + dispatcher.Dispatch(_jsons.GetWithoutBoundsChecks(baseIndex + 5).AsSpanFast(), ref extraData); + dispatcher.Dispatch(_jsons.GetWithoutBoundsChecks(baseIndex + 6).AsSpanFast(), ref extraData); + dispatcher.Dispatch(_jsons.GetWithoutBoundsChecks(baseIndex + 7).AsSpanFast(), ref extraData); + dispatcher.Dispatch(_jsons.GetWithoutBoundsChecks(baseIndex + 8).AsSpanFast(), ref extraData); + dispatcher.Dispatch(_jsons.GetWithoutBoundsChecks(baseIndex + 9).AsSpanFast(), ref extraData); + } + } +} \ No newline at end of file diff --git a/Source/Reloaded.Messaging.Benchmarks/MessagePackingRealScenarioBenchmark.cs b/Source/Reloaded.Messaging.Benchmarks/MessagePackingRealScenarioBenchmark.cs new file mode 100644 index 0000000..8881da7 --- /dev/null +++ b/Source/Reloaded.Messaging.Benchmarks/MessagePackingRealScenarioBenchmark.cs @@ -0,0 +1,109 @@ +using BenchmarkDotNet.Attributes; +using Reloaded.Messaging.Benchmarks.Structures; +using Microsoft.IO; +using Reloaded.Messaging.Benchmarks.Utilities; +using Reloaded.Messaging.Interfaces; +using Reloaded.Messaging.Messages; +using Reloaded.Messaging.Utilities; +using Reloaded.Messaging.Extras.Runtime; +using Reloaded.Messaging.Interfaces.Utilities; + +namespace Reloaded.Messaging.Benchmarks; + +[MemoryDiagnoser] +public class MessagePackingRealScenarioBenchmark +{ + private ModConfigMessage[]? _items; + + private RecyclableMemoryStreamManager _streamManager = new(); + private MessageDispatcher _dispatcher; + private SourceGeneratedSystemTextJsonSerializer _srcGenSystemTextJsonSerializer; + + [Params(Constants.DefaultOperationCount)] + public int NumItems { get; set; } + + [GlobalSetup] + public void Setup() + { + _srcGenSystemTextJsonSerializer = new SourceGeneratedSystemTextJsonSerializer(ModConfigMessageContext.Default.ModConfigMessage); + _dispatcher = new MessageDispatcher(); + _items = ModConfig.Create(NumItems); + GC.Collect(); + } + + [GlobalCleanup] + public void Cleanup() + { + _items = null; + GC.Collect(); + } + + + [Benchmark(Baseline = true)] + public void SerializeOnly_NoPack_To_SingleBuffer() + { + using var buffer = (RecyclableMemoryStream)_streamManager.GetStream(); + for (int x = 0; x < _items!.Length; x++) + _srcGenSystemTextJsonSerializer.Serialize(ref _items.GetWithoutBoundsChecks(x), buffer); + } + + [Benchmark] + public void SerializeOnly_NoPack_To_BufferPerMessage() + { + for (int x = 0; x < _items!.Length; x++) + { + using var buffer = (RecyclableMemoryStream)_streamManager.GetStream(); + _srcGenSystemTextJsonSerializer.Serialize(ref _items.GetWithoutBoundsChecks(x), buffer); + } + } + + [Benchmark] + public void Serialize_And_Pack() + { + var dummy = new ModConfigMessage(); + for (int x = 0; x < _items!.Length; x++) + { + using var serialized = dummy.Serialize(ref _items.GetWithoutBoundsChecks(x)); + // Calling span might lead to a memory copy operation, hence to make the + // test fair, we need to call it on the baseline too. + var _ = serialized.Span; + } + } + + [Benchmark] + public void Serialize_And_Pack_And_Handle() + { + // Copy should be cheap here, dispatcher is small. + var dispatcher = _dispatcher; + dispatcher.AddOrOverrideHandler(new DummyMessageHandlerNoDeserialize, NullCompressor>()); + var dummyMsg = new ModConfigMessage(); + int dummy = 0; + + for (int x = 0; x < _items!.Length; x++) + { + using var serialized = dummyMsg.Serialize(ref _items.GetWithoutBoundsChecks(x)); + dispatcher.Dispatch(serialized.Span, ref dummy); + } + + dispatcher.RemoveHandler(ModConfigMessage.MessageType); + } + + [Benchmark] + public void Serialize_And_Pack_And_Handle_And_Unpack_And_Deserialize() + { + // Copy should be cheap here, dispatcher is small. + var dispatcher = _dispatcher; + _items[0].AddToDispatcher(new DummyCallback(), ref _dispatcher); + + var dummyMsg = new ModConfigMessage(); + int dummy = 0; + + for (int x = 0; x < _items!.Length; x++) + { + using var serialized = dummyMsg.Serialize(ref _items.GetWithoutBoundsChecks(x)); + dispatcher.Dispatch(serialized.Span, ref dummy); + } + + dispatcher.RemoveHandler(ModConfigMessage.MessageType); + } +} \ No newline at end of file diff --git a/Source/Reloaded.Messaging.Benchmarks/PackOverheadBenchmark.cs b/Source/Reloaded.Messaging.Benchmarks/PackOverheadBenchmark.cs new file mode 100644 index 0000000..3f28d2b --- /dev/null +++ b/Source/Reloaded.Messaging.Benchmarks/PackOverheadBenchmark.cs @@ -0,0 +1,112 @@ +using System.Runtime.CompilerServices; +using BenchmarkDotNet.Attributes; +using Microsoft.IO; +using Reloaded.Messaging.Benchmarks.Structures; +using Reloaded.Messaging.Benchmarks.Utilities; +using Reloaded.Messaging.Interfaces.Utilities; +using Reloaded.Messaging.Messages; + +namespace Reloaded.Messaging.Benchmarks; + +[MemoryDiagnoser] +public class PackOverheadBenchmark +{ + private ModConfigMessageWithDummySerializer[]? _items; + private ModConfigMessageWithDummyCompressor[]? _itemsCompressor; + + private RecyclableMemoryStream _stream; + private RecyclableMemoryStreamManager _streamManager = new(); + private DummySerializer _dummySerializer; + + [Params(Constants.DefaultOperationCount)] + public int NumItems { get; set; } + + private const int UnrollFactor = 10; + + [GlobalSetup] + public void Setup() + { + _stream = (RecyclableMemoryStream)_streamManager.GetStream(); + _dummySerializer = new(); + _items = ModConfig.Create(NumItems); + _itemsCompressor = ModConfig.Create(NumItems); + GC.Collect(); + } + + [GlobalCleanup] + public void Cleanup() + { + _items = null; + GC.Collect(); + } + + + [Benchmark(Baseline = true)] + [SkipLocalsInit] + public void DummySerializeOnly() + { + var numIterations = _items!.Length / UnrollFactor; + var serializer = _dummySerializer; + + for (int x = 0; x < numIterations; x++) + { + var baseIndex = x * UnrollFactor; + serializer.Serialize(ref _items.GetWithoutBoundsChecks(baseIndex + 0), _stream); + serializer.Serialize(ref _items.GetWithoutBoundsChecks(baseIndex + 1), _stream); + serializer.Serialize(ref _items.GetWithoutBoundsChecks(baseIndex + 2), _stream); + serializer.Serialize(ref _items.GetWithoutBoundsChecks(baseIndex + 3), _stream); + serializer.Serialize(ref _items.GetWithoutBoundsChecks(baseIndex + 4), _stream); + serializer.Serialize(ref _items.GetWithoutBoundsChecks(baseIndex + 5), _stream); + serializer.Serialize(ref _items.GetWithoutBoundsChecks(baseIndex + 6), _stream); + serializer.Serialize(ref _items.GetWithoutBoundsChecks(baseIndex + 7), _stream); + serializer.Serialize(ref _items.GetWithoutBoundsChecks(baseIndex + 8), _stream); + serializer.Serialize(ref _items.GetWithoutBoundsChecks(baseIndex + 9), _stream); + } + } + + [Benchmark] + [SkipLocalsInit] + public void DummySerialize_And_Pack() + { + var numIterations = _items!.Length / UnrollFactor; + + for (int x = 0; x < numIterations; x++) + { + var baseIndex = x * UnrollFactor; + MessageWriter, NullCompressor>.SerializeToRecyclableMemoryStream(ref _items!.GetWithoutBoundsChecks(baseIndex + 0), _stream).Dispose(); + MessageWriter, NullCompressor>.SerializeToRecyclableMemoryStream(ref _items!.GetWithoutBoundsChecks(baseIndex + 1), _stream).Dispose(); + MessageWriter, NullCompressor>.SerializeToRecyclableMemoryStream(ref _items!.GetWithoutBoundsChecks(baseIndex + 2), _stream).Dispose(); + MessageWriter, NullCompressor>.SerializeToRecyclableMemoryStream(ref _items!.GetWithoutBoundsChecks(baseIndex + 3), _stream).Dispose(); + MessageWriter, NullCompressor>.SerializeToRecyclableMemoryStream(ref _items!.GetWithoutBoundsChecks(baseIndex + 4), _stream).Dispose(); + MessageWriter, NullCompressor>.SerializeToRecyclableMemoryStream(ref _items!.GetWithoutBoundsChecks(baseIndex + 5), _stream).Dispose(); + MessageWriter, NullCompressor>.SerializeToRecyclableMemoryStream(ref _items!.GetWithoutBoundsChecks(baseIndex + 6), _stream).Dispose(); + MessageWriter, NullCompressor>.SerializeToRecyclableMemoryStream(ref _items!.GetWithoutBoundsChecks(baseIndex + 7), _stream).Dispose(); + MessageWriter, NullCompressor>.SerializeToRecyclableMemoryStream(ref _items!.GetWithoutBoundsChecks(baseIndex + 8), _stream).Dispose(); + MessageWriter, NullCompressor>.SerializeToRecyclableMemoryStream(ref _items!.GetWithoutBoundsChecks(baseIndex + 9), _stream).Dispose(); + } + } + + [Benchmark] + [SkipLocalsInit] + public void DummySerialize_And_Pack_Compressed() + { + // This test is OK because the actual value isn't serialized. + var numIterations = _itemsCompressor!.Length / UnrollFactor; + var compressor = new DummyCompressor(); + + for (int x = 0; x < numIterations; x++) + { + var baseIndex = x * UnrollFactor; + MessageWriter, DummyCompressor>.SerializeToRecyclableMemoryStream(ref _itemsCompressor!.GetWithoutBoundsChecks(baseIndex + 0), _stream, compressor).Dispose(); + MessageWriter, DummyCompressor>.SerializeToRecyclableMemoryStream(ref _itemsCompressor!.GetWithoutBoundsChecks(baseIndex + 1), _stream, compressor).Dispose(); + MessageWriter, DummyCompressor>.SerializeToRecyclableMemoryStream(ref _itemsCompressor!.GetWithoutBoundsChecks(baseIndex + 2), _stream, compressor).Dispose(); + MessageWriter, DummyCompressor>.SerializeToRecyclableMemoryStream(ref _itemsCompressor!.GetWithoutBoundsChecks(baseIndex + 3), _stream, compressor).Dispose(); + MessageWriter, DummyCompressor>.SerializeToRecyclableMemoryStream(ref _itemsCompressor!.GetWithoutBoundsChecks(baseIndex + 4), _stream, compressor).Dispose(); + MessageWriter, DummyCompressor>.SerializeToRecyclableMemoryStream(ref _itemsCompressor!.GetWithoutBoundsChecks(baseIndex + 5), _stream, compressor).Dispose(); + MessageWriter, DummyCompressor>.SerializeToRecyclableMemoryStream(ref _itemsCompressor!.GetWithoutBoundsChecks(baseIndex + 6), _stream, compressor).Dispose(); + MessageWriter, DummyCompressor>.SerializeToRecyclableMemoryStream(ref _itemsCompressor!.GetWithoutBoundsChecks(baseIndex + 7), _stream, compressor).Dispose(); + MessageWriter, DummyCompressor>.SerializeToRecyclableMemoryStream(ref _itemsCompressor!.GetWithoutBoundsChecks(baseIndex + 8), _stream, compressor).Dispose(); + MessageWriter, DummyCompressor>.SerializeToRecyclableMemoryStream(ref _itemsCompressor!.GetWithoutBoundsChecks(baseIndex + 9), _stream, compressor).Dispose(); + } + } +} \ No newline at end of file diff --git a/Source/Reloaded.Messaging.Benchmarks/Program.cs b/Source/Reloaded.Messaging.Benchmarks/Program.cs new file mode 100644 index 0000000..fbf1ead --- /dev/null +++ b/Source/Reloaded.Messaging.Benchmarks/Program.cs @@ -0,0 +1,56 @@ +// See https://aka.ms/new-console-template for more information + +using BenchmarkDotNet.Columns; +using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Jobs; +using BenchmarkDotNet.Reports; +using BenchmarkDotNet.Running; +using BenchmarkDotNet.Toolchains.InProcess.Emit; +using Reloaded.Messaging.Benchmarks; +using Reloaded.Messaging.Benchmarks.Utilities; + +BenchmarkRunner.Run(new InProcessConfig(new OperationsPerSecondColumn())); +BenchmarkRunner.Run(new InProcessConfig(new OperationsPerSecondColumn())); +BenchmarkRunner.Run(new InProcessConfig(new OperationsPerSecondColumn())); +BenchmarkRunner.Run(new InProcessConfig(new OperationsPerSecondColumn())); +BenchmarkRunner.Run(new InProcessConfig(new OperationsPerSecondColumn())); + +public class InProcessConfig : ManualConfig +{ + public InProcessConfig(params IColumn[] extraColumns) + { + Add(DefaultConfig.Instance); + foreach (var column in extraColumns) + AddColumn(column); + + AddJob(Job.Default + .WithToolchain(InProcessEmitToolchain.Instance) + .WithId(".NET (Current Process)")); + } +} + +public class OperationsPerSecondColumn : IColumn +{ + public string Id { get; } = nameof(OperationsPerSecondColumn); + public string ColumnName { get; } = "Operations/s"; + public bool AlwaysShow { get; } = true; + public ColumnCategory Category { get; } = ColumnCategory.Custom; + public int PriorityInCategory { get; } + public bool IsNumeric { get; } = true; + public UnitType UnitType { get; } = UnitType.Size; + public string Legend { get; } + + public string GetValue(Summary summary, BenchmarkCase benchmarkCase) + { + var ourReport = summary.Reports.First(x => x.BenchmarkCase.Equals(benchmarkCase)); + var mean = ourReport.ResultStatistics.Mean; + var meanSeconds = mean / 1000_000_000F; + + return $"{(Constants.DefaultOperationCount / meanSeconds):#####.00}"; + } + + public string GetValue(Summary summary, BenchmarkCase benchmarkCase, SummaryStyle style) => GetValue(summary, benchmarkCase); + + public bool IsDefault(Summary summary, BenchmarkCase benchmarkCase) => false; + public bool IsAvailable(Summary summary) => true; +} \ No newline at end of file diff --git a/Source/Reloaded.Messaging.Benchmarks/Reloaded.Messaging.Benchmarks.csproj b/Source/Reloaded.Messaging.Benchmarks/Reloaded.Messaging.Benchmarks.csproj new file mode 100644 index 0000000..6f1dc02 --- /dev/null +++ b/Source/Reloaded.Messaging.Benchmarks/Reloaded.Messaging.Benchmarks.csproj @@ -0,0 +1,24 @@ + + + + Exe + net6.0 + enable + enable + true + + + + + + + + + + + + + + + + diff --git a/Source/Reloaded.Messaging.Benchmarks/SerializationBenchmark.cs b/Source/Reloaded.Messaging.Benchmarks/SerializationBenchmark.cs new file mode 100644 index 0000000..0c45a2d --- /dev/null +++ b/Source/Reloaded.Messaging.Benchmarks/SerializationBenchmark.cs @@ -0,0 +1,73 @@ +using System.Runtime.InteropServices; +using BenchmarkDotNet.Attributes; +using Microsoft.IO; +using Reloaded.Messaging.Benchmarks.Structures; +using Reloaded.Messaging.Benchmarks.Utilities; +using Reloaded.Messaging.Extras.Runtime; +using Reloaded.Messaging.Interfaces; +using Reloaded.Messaging.Serializer.MessagePack; + +namespace Reloaded.Messaging.Benchmarks; + +[MemoryDiagnoser] +public class SerializationBenchmark +{ + public const int UnrollFactor = 5; + + [Params(Constants.DefaultOperationCount)] + public int NumItems { get; set; } + + private ModConfig[]? _items; + private RecyclableMemoryStreamManager _streamManager = new(); + private RecyclableMemoryStream _stream; + + private SourceGeneratedSystemTextJsonSerializer _srcGenSystemTextJsonSerializer; + private SystemTextJsonSerializer _systemTextJsonSerializer = new(); + private MessagePackSerializer _messagePackSerializer = new(); + + [GlobalSetup] + public void Setup() + { + _srcGenSystemTextJsonSerializer = new(ModConfigContext.Default.ModConfig); + + _items = ModConfig.Create(NumItems); + _stream = (RecyclableMemoryStream)_streamManager.GetStream(); + GC.Collect(); + } + + [GlobalCleanup] + public void Cleanup() + { + _items = null; + GC.Collect(); + } + + [IterationCleanup] + public void IterationSetup() + { + _stream.Seek(0, SeekOrigin.Begin); + } + + [Benchmark] + public void SystemTextJson() => BenchmarkCommon(_systemTextJsonSerializer); + + [Benchmark] + public void SystemTextJsonSrcGen() => BenchmarkCommon(_srcGenSystemTextJsonSerializer); + + [Benchmark] + public void MessagePack() => BenchmarkCommon(_messagePackSerializer); + + private void BenchmarkCommon(TSerializer serializer) where TSerializer : ISerializer + { + var numIterations = _items.Length / UnrollFactor; + for (int x = 0; x < numIterations; x++) + { + var baseIndex = x * UnrollFactor; + serializer.Serialize(ref _items.GetWithoutBoundsChecks(baseIndex + 0), _stream); + serializer.Serialize(ref _items.GetWithoutBoundsChecks(baseIndex + 1), _stream); + serializer.Serialize(ref _items.GetWithoutBoundsChecks(baseIndex + 2), _stream); + serializer.Serialize(ref _items.GetWithoutBoundsChecks(baseIndex + 3), _stream); + serializer.Serialize(ref _items.GetWithoutBoundsChecks(baseIndex + 4), _stream); + } + } +} \ No newline at end of file diff --git a/Source/Reloaded.Messaging.Benchmarks/Structures/ModConfig.cs b/Source/Reloaded.Messaging.Benchmarks/Structures/ModConfig.cs new file mode 100644 index 0000000..a291b80 --- /dev/null +++ b/Source/Reloaded.Messaging.Benchmarks/Structures/ModConfig.cs @@ -0,0 +1,104 @@ +using Bogus; +using Reloaded.Messaging.Extras.Runtime; +using Reloaded.Messaging.Interfaces; +using Reloaded.Messaging.Interfaces.Utilities; +using Reloaded.Messaging.Serializer.MessagePack; +using System.Text.Json.Serialization; +using Reloaded.Messaging.Benchmarks.Utilities; + +namespace Reloaded.Messaging.Benchmarks.Structures; + +/// +/// Copy of Reloaded-II's ModConfig +/// +public class ModConfig +{ + + /* Class members. */ + public string? ModId { get; set; } + public string? ModName { get; set; } + public string? ModAuthor { get; set; } + public string? ModVersion { get; set; } + public string? ModDescription { get; set; } + public string? ModDll { get; set; } + + public string? ModIcon { get; set; } + public string? ModR2RManagedDll32 { get; set; } + public string? ModR2RManagedDll64 { get; set; } + public string? ModNativeDll32 { get; set; } + public string? ModNativeDll64 { get; set; } + public bool? IsLibrary { get; set; } + public string? ReleaseMetadataFileName { get; set; } + + public Dictionary? PluginData { get; set; } + + public bool? IsUniversalMod { get; set; } + + public string[]? ModDependencies { get; set; } + public string[]? OptionalDependencies { get; set; } + public string[]? SupportedAppId { get; set; } + + public static T[] Create(int numConfigs) where T : ModConfig, new() + { + var dependencies = new Faker().CustomInstantiator(f => f.Hacker.Random.Hash()).GenerateArray(numConfigs); + var applications = new Faker().CustomInstantiator(f => f.System.FileName(".exe")).GenerateArray(Math.Max(numConfigs / 100, 1)); + + var faker = new Faker().CustomInstantiator(f => + { + var x = new T(); + x.ModId = f.Hacker.Random.Hash(); + x.ModName = f.Name.FullName(); + x.ModAuthor = f.Internet.UserName(); + x.ModVersion = f.System.Version().ToString(); + x.ModDescription = f.Lorem.Sentences(3); + x.ModDll = f.System.FileName(".dll"); + x.ModIcon = f.System.CommonFileName(".png"); + x.ModR2RManagedDll32 = f.System.FileName(".dll").OrNull(f, 0.8f); + x.ModR2RManagedDll64 = f.System.FileName(".dll").OrNull(f, 0.8f); + x.ModNativeDll32 = f.System.FileName(".dll").OrNull(f, 0.8f); + x.ModNativeDll64 = f.System.FileName(".dll").OrNull(f, 0.8f); + x.IsLibrary = f.Random.Bool(0.01f); + x.ReleaseMetadataFileName = f.System.FileName(".json"); + x.IsLibrary = f.Random.Bool(0.01f); + x.ModDependencies = f.Random.ArrayElementsFast(dependencies, f.Random.Int(0, 5)); + x.OptionalDependencies = f.Random.ArrayElementsFast(dependencies, f.Random.Int(0, 1)); + x.SupportedAppId = f.Random.ArrayElementsFast(applications, 1); + return x; + }); + + return faker.GenerateArray(numConfigs); + } +} + +public class ModConfigMessage : ModConfig, IMessage, NullCompressor> +{ + public const int MessageType = 0; + + public SourceGeneratedSystemTextJsonSerializer GetSerializer() => new (ModConfigMessageContext.Default.ModConfigMessage); + public NullCompressor GetCompressor() => null; + public sbyte GetMessageType() => MessageType; +} + +public class ModConfigMessageWithDummySerializer : ModConfig, IMessage, NullCompressor> +{ + public const int MessageType = 0; + + public DummySerializer GetSerializer() => new(); + public NullCompressor GetCompressor() => null; + public sbyte GetMessageType() => MessageType; +} + +public class ModConfigMessageWithDummyCompressor: ModConfig, IMessage, DummyCompressor> +{ + public const int MessageType = 0; + + public DummySerializer GetSerializer() => new(); + public DummyCompressor GetCompressor() => new(); + public sbyte GetMessageType() => MessageType; +} + +[JsonSerializable(typeof(ModConfigMessage), GenerationMode = JsonSourceGenerationMode.Default)] +internal partial class ModConfigMessageContext : JsonSerializerContext { } + +[JsonSerializable(typeof(ModConfig), GenerationMode = JsonSourceGenerationMode.Default)] +internal partial class ModConfigContext : JsonSerializerContext { } \ No newline at end of file diff --git a/Source/Reloaded.Messaging.Benchmarks/Utilities/ArrayExtensions.cs b/Source/Reloaded.Messaging.Benchmarks/Utilities/ArrayExtensions.cs new file mode 100644 index 0000000..d39c42b --- /dev/null +++ b/Source/Reloaded.Messaging.Benchmarks/Utilities/ArrayExtensions.cs @@ -0,0 +1,14 @@ +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace Reloaded.Messaging.Benchmarks.Utilities; + +public static class ArrayExtensions +{ + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ref T GetWithoutBoundsChecks(this T[] items, int index) + { + return ref Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(items), index); + } +} \ No newline at end of file diff --git a/Source/Reloaded.Messaging.Benchmarks/Utilities/Constants.cs b/Source/Reloaded.Messaging.Benchmarks/Utilities/Constants.cs new file mode 100644 index 0000000..b13edb6 --- /dev/null +++ b/Source/Reloaded.Messaging.Benchmarks/Utilities/Constants.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Reloaded.Messaging.Benchmarks.Utilities; + +public static class Constants +{ + public const int DefaultOperationCount = 100000; +} \ No newline at end of file diff --git a/Source/Reloaded.Messaging.Benchmarks/Utilities/DummyCompressor.cs b/Source/Reloaded.Messaging.Benchmarks/Utilities/DummyCompressor.cs new file mode 100644 index 0000000..ce46d22 --- /dev/null +++ b/Source/Reloaded.Messaging.Benchmarks/Utilities/DummyCompressor.cs @@ -0,0 +1,29 @@ +using Reloaded.Messaging.Interfaces; + +namespace Reloaded.Messaging.Benchmarks.Utilities; + +/// +/// Dummy compressor that performs no compression. +/// Use me when specifying TCompressor and return null in structures. +/// +public struct DummyCompressor : ICompressor +{ + /// + public int GetMaxCompressedSize(int inputSize) + { + return inputSize; + } + + /// + public int Compress(Span uncompressedData, Span compressedData) + { + uncompressedData.CopyTo(compressedData); + return uncompressedData.Length; + } + + /// + public void Decompress(Span compressedBuf, Span uncompressedBuf) + { + compressedBuf.CopyTo(uncompressedBuf); + } +} \ No newline at end of file diff --git a/Source/Reloaded.Messaging.Benchmarks/Utilities/DummyMessageHandlers.cs b/Source/Reloaded.Messaging.Benchmarks/Utilities/DummyMessageHandlers.cs new file mode 100644 index 0000000..813a436 --- /dev/null +++ b/Source/Reloaded.Messaging.Benchmarks/Utilities/DummyMessageHandlers.cs @@ -0,0 +1,21 @@ +using Reloaded.Messaging.Benchmarks.Structures; +using Reloaded.Messaging.Interfaces; + +namespace Reloaded.Messaging.Benchmarks.Utilities; + +public class DummyMessageHandlerNoDeserialize : IMessageHandlerBase + where TStruct : IMessage + where TCompressor : ICompressor + where TSerializer : ISerializer +{ + public sbyte GetMessageType() => 0; + public void HandleMessage(Span data, int decompressedSize, ref TExtraData extraData) + { + // No code + } +} + +public struct DummyCallback : IMsgRefAction +{ + public void OnMessageReceive(ref ModConfigMessage received, ref int data) { } +} \ No newline at end of file diff --git a/Source/Reloaded.Messaging.Benchmarks/Utilities/DummySerializer.cs b/Source/Reloaded.Messaging.Benchmarks/Utilities/DummySerializer.cs new file mode 100644 index 0000000..de15f24 --- /dev/null +++ b/Source/Reloaded.Messaging.Benchmarks/Utilities/DummySerializer.cs @@ -0,0 +1,16 @@ +using System.Buffers; +using Reloaded.Messaging.Interfaces; + +namespace Reloaded.Messaging.Benchmarks.Utilities; + +public struct DummySerializer : ISerializer where TStruct : new() +{ + public TStruct Deserialize(Span serialized) + { + return new TStruct(); + } + + public void Serialize(ref TStruct item, IBufferWriter writer) + { + } +} \ No newline at end of file diff --git a/Source/Reloaded.Messaging.Benchmarks/Utilities/FakerExtensions.cs b/Source/Reloaded.Messaging.Benchmarks/Utilities/FakerExtensions.cs new file mode 100644 index 0000000..ab6219c --- /dev/null +++ b/Source/Reloaded.Messaging.Benchmarks/Utilities/FakerExtensions.cs @@ -0,0 +1,28 @@ +using System.Runtime; +using Bogus; + +namespace Reloaded.Messaging.Benchmarks.Utilities; + +public static class FakerExtensions +{ + public static Random GlobalRandom = Random.Shared; + + public static T[] GenerateArray(this Faker faker, int num) where T : class + { + var items = GC.AllocateUninitializedArray(num); + for (int x = 0; x < items.Length; x++) + items[x] = faker.Generate(); + + return items; + } + + public static T[] ArrayElementsFast(this Randomizer random, T[] values, int num) where T : class + { + var elements = new T[num]; + + for (int x = 0; x < num; x++) + elements[x] = values[GlobalRandom.Next(0, num)]; + + return elements; + } +} \ No newline at end of file diff --git a/Source/Reloaded.Messaging.Benchmarks/Utilities/SpanExtensions.cs b/Source/Reloaded.Messaging.Benchmarks/Utilities/SpanExtensions.cs new file mode 100644 index 0000000..24976f3 --- /dev/null +++ b/Source/Reloaded.Messaging.Benchmarks/Utilities/SpanExtensions.cs @@ -0,0 +1,28 @@ +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace Reloaded.Messaging.Benchmarks.Utilities; + +/// +/// Extension methods related to spans. +/// +public static class SpanExtensions +{ + /// + /// Provides zero overhead unsafe array to span conversion. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Span AsSpanFast(this T[] data) + { + return MemoryMarshal.CreateSpan(ref MemoryMarshal.GetArrayDataReference(data), data.Length); + } + + /// + /// Provides zero overhead unsafe array to span conversion. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Span AsSpanFast(this T[] data, int length) + { + return MemoryMarshal.CreateSpan(ref MemoryMarshal.GetArrayDataReference(data), length); + } +} \ No newline at end of file diff --git a/Source/Reloaded.Messaging.Compressor.ZStandard/Reloaded.Messaging.Compressor.ZStandard.csproj b/Source/Reloaded.Messaging.Compressor.ZStandard/Reloaded.Messaging.Compressor.ZStandard.csproj index 5c469cb..a2251c1 100644 --- a/Source/Reloaded.Messaging.Compressor.ZStandard/Reloaded.Messaging.Compressor.ZStandard.csproj +++ b/Source/Reloaded.Messaging.Compressor.ZStandard/Reloaded.Messaging.Compressor.ZStandard.csproj @@ -1,7 +1,7 @@  - netstandard2.1;netstandard2.0 + netstandard2.1;netstandard2.0;netcoreapp3.1;net5.0 preview Sewer56 LICENSE.md diff --git a/Source/Reloaded.Messaging.Compressor.ZStandard/ZStandardCompressor.cs b/Source/Reloaded.Messaging.Compressor.ZStandard/ZStandardCompressor.cs index 1ec904f..003ab56 100644 --- a/Source/Reloaded.Messaging.Compressor.ZStandard/ZStandardCompressor.cs +++ b/Source/Reloaded.Messaging.Compressor.ZStandard/ZStandardCompressor.cs @@ -7,7 +7,7 @@ namespace Reloaded.Messaging.Compressor.ZStandard; /// /// Creates a new compressor using ZStd. /// -public class ZStandardCompressor : ICompressor, IDisposable +public struct ZStandardCompressor : ICompressor, IDisposable { /// /// Instance of the compressor. @@ -30,10 +30,13 @@ public ZStandardCompressor(CompressionOptions compressionOptions = null, Decompr Decompressor = decompressionOptions != null ? new Decompressor(decompressionOptions) : new Decompressor(); } - /// - ~ZStandardCompressor() + /// + /// Creates a new compressor based off of ZStandard. + /// + public ZStandardCompressor() { - Dispose(); + Compressor = new ZstdNet.Compressor(); + Decompressor = new Decompressor(); } /// @@ -41,19 +44,14 @@ public void Dispose() { Compressor?.Dispose(); Decompressor?.Dispose(); - GC.SuppressFinalize(this); } + /// + public int GetMaxCompressedSize(int inputSize) => ZstdNet.Compressor.GetCompressBound(inputSize); /// - public byte[] Compress(byte[] data) - { - return Compressor.Wrap(data); - } + public int Compress(Span uncompressed, Span compressed) => Compressor.Wrap(uncompressed, compressed); /// - public byte[] Decompress(byte[] data) - { - return Decompressor.Unwrap(data); - } + public void Decompress(Span compressedData, Span uncompressedData) => Decompressor.Unwrap(compressedData, uncompressedData); } \ No newline at end of file diff --git a/Source/Reloaded.Messaging.Extras.Runtime/BrotliCompressor.cs b/Source/Reloaded.Messaging.Extras.Runtime/BrotliCompressor.cs new file mode 100644 index 0000000..672819b --- /dev/null +++ b/Source/Reloaded.Messaging.Extras.Runtime/BrotliCompressor.cs @@ -0,0 +1,53 @@ +using System; +using System.IO; +using System.IO.Compression; +using Reloaded.Messaging.Interfaces; + +namespace Reloaded.Messaging.Extras.Runtime; + +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER +/// +/// Provides brotli compression support. +/// +public struct BrotliCompressor : ICompressor +{ + private byte _quality; + private byte _window; + + /// + /// Creates the default brotli compressor. + /// + public BrotliCompressor() + { + _window = 22; + _quality = 9; + } + + /// + /// Quality of encoder. Between 0 and 11. Recommend 9 for size/speed ratio. + /// Size of window. + public BrotliCompressor(byte quality, byte window = 22) + { + _quality = quality; + _window = window; + } + + /// + public int GetMaxCompressedSize(int inputSize) => BrotliEncoder.GetMaxCompressedLength(inputSize); + + /// + public int Compress(Span uncompressedData, Span compressedData) + { + using var encoder = new BrotliEncoder(_quality, _window); + encoder.Compress(uncompressedData, compressedData, out _, out var bytesWritten, true); + return bytesWritten; + } + + /// + public void Decompress(Span compressedBuf, Span uncompressedBuf) + { + using var decoder = new BrotliDecoder(); + decoder.Decompress(compressedBuf, uncompressedBuf, out _, out _); + } +} +#endif \ No newline at end of file diff --git a/Source/Reloaded.Messaging.Extras.Runtime/Pool.cs b/Source/Reloaded.Messaging.Extras.Runtime/Pool.cs new file mode 100644 index 0000000..cf2f226 --- /dev/null +++ b/Source/Reloaded.Messaging.Extras.Runtime/Pool.cs @@ -0,0 +1,22 @@ +using System; +using System.IO; +using System.Text.Json; + +namespace Reloaded.Messaging.Extras.Runtime; + +internal static class Pool +{ + [ThreadStatic] + private static Utf8JsonWriter? _sharedWriter; + + internal static Utf8JsonWriter JsonWriterPerThread() + { + if (_sharedWriter == null) + { + _sharedWriter = new Utf8JsonWriter(Stream.Null); + return _sharedWriter; + } + + return _sharedWriter; + } +} \ No newline at end of file diff --git a/Source/Reloaded.Messaging.Serializer.NewtonsoftJson/Reloaded.Messaging.Serializer.NewtonsoftJson.csproj b/Source/Reloaded.Messaging.Extras.Runtime/Reloaded.Messaging.Extras.Runtime.csproj similarity index 75% rename from Source/Reloaded.Messaging.Serializer.NewtonsoftJson/Reloaded.Messaging.Serializer.NewtonsoftJson.csproj rename to Source/Reloaded.Messaging.Extras.Runtime/Reloaded.Messaging.Extras.Runtime.csproj index d8a3cf6..5262553 100644 --- a/Source/Reloaded.Messaging.Serializer.NewtonsoftJson/Reloaded.Messaging.Serializer.NewtonsoftJson.csproj +++ b/Source/Reloaded.Messaging.Extras.Runtime/Reloaded.Messaging.Extras.Runtime.csproj @@ -1,40 +1,36 @@  - netstandard2.0 + netcoreapp3.1;net6.0 preview - Reloaded.Messaging.Serializer.NewtonsoftJson Sewer56 Sewer56 - Basic Json serialization implementation for Reloaded.Messaging based off of Newtonsoft.Json. + Provides support for compressors and serializers available in the .NET runtime. Sewer56 LICENSE.md https://github.com/Reloaded-Project/Reloaded.Messaging https://github.com/Reloaded-Project/Reloaded.Messaging true true - 2.0.0 NuGet-Icon.png + enable + true - - - - - - True - - True \ + + True + + diff --git a/Source/Reloaded.Messaging.Extras.Runtime/SourceGeneratedSystemTextJsonSerializer.cs b/Source/Reloaded.Messaging.Extras.Runtime/SourceGeneratedSystemTextJsonSerializer.cs new file mode 100644 index 0000000..b582ffd --- /dev/null +++ b/Source/Reloaded.Messaging.Extras.Runtime/SourceGeneratedSystemTextJsonSerializer.cs @@ -0,0 +1,45 @@ +namespace Reloaded.Messaging.Extras.Runtime; + +#if NET6_0_OR_GREATER +using System; +using System.Text.Json; +using System.Text.Json.Serialization.Metadata; +using Reloaded.Messaging.Interfaces; +using System.Buffers; +using System.Runtime.CompilerServices; + +/// +public struct SourceGeneratedSystemTextJsonSerializer : ISerializer +{ + private readonly JsonTypeInfo _typeInfo; + + /// + /// Creates the System.Text.Json based serializer. + /// + /// Source generated JSON type information. + public SourceGeneratedSystemTextJsonSerializer(JsonTypeInfo typeInfo) + { + _typeInfo = typeInfo; + } + + /// +#if NET5_0_OR_GREATER + [SkipLocalsInit] +#endif + public TStruct Deserialize(Span serialized) + { + return JsonSerializer.Deserialize(serialized, _typeInfo)!; + } + + /// +#if NET5_0_OR_GREATER + [SkipLocalsInit] +#endif + public void Serialize(ref TStruct item, IBufferWriter writer) + { + var write = Pool.JsonWriterPerThread(); + write.Reset(writer); + JsonSerializer.Serialize(write, item, _typeInfo); + } +} +#endif \ No newline at end of file diff --git a/Source/Reloaded.Messaging.Extras.Runtime/SystemTextJsonSerializer.cs b/Source/Reloaded.Messaging.Extras.Runtime/SystemTextJsonSerializer.cs new file mode 100644 index 0000000..8641027 --- /dev/null +++ b/Source/Reloaded.Messaging.Extras.Runtime/SystemTextJsonSerializer.cs @@ -0,0 +1,54 @@ +using System; +using System.Buffers; +using System.IO; +using System.Runtime.CompilerServices; +using System.Text.Json; +using Reloaded.Messaging.Interfaces; + +namespace Reloaded.Messaging.Extras.Runtime; + +/// +public struct SystemTextJsonSerializer : ISerializer +{ + /// + /// Serialization options. + /// + public JsonSerializerOptions Options { get; private set; } + + /// + /// Creates the System.Text.Json based serializer. + /// + public SystemTextJsonSerializer() + { + Options = new JsonSerializerOptions(); + } + + /// + /// Creates the System.Text.Json based serializer. + /// + /// Options to use for serialization/deserialization. + public SystemTextJsonSerializer(JsonSerializerOptions serializerOptions) + { + Options = serializerOptions; + } + + /// +#if NET5_0_OR_GREATER + [SkipLocalsInit] +#endif + public TStruct Deserialize(Span serialized) + { + return JsonSerializer.Deserialize(serialized, Options)!; + } + + /// +#if NET5_0_OR_GREATER + [SkipLocalsInit] +#endif + public void Serialize(ref TStruct item, IBufferWriter writer) + { + var write = Pool.JsonWriterPerThread(); + write.Reset(writer); + JsonSerializer.Serialize(write, item, Options); + } +} \ No newline at end of file diff --git a/Source/Reloaded.Messaging.Host.LiteNetLib/LiteNetLibHost.cs b/Source/Reloaded.Messaging.Host.LiteNetLib/LiteNetLibHost.cs new file mode 100644 index 0000000..b6afabb --- /dev/null +++ b/Source/Reloaded.Messaging.Host.LiteNetLib/LiteNetLibHost.cs @@ -0,0 +1,138 @@ +using System; +using System.Runtime.CompilerServices; +using LiteNetLib; +using LiteNetLib.Utils; +using Reloaded.Messaging.Interfaces; +#if NET5_0_OR_GREATER +using System.Runtime.InteropServices; +#endif + +namespace Reloaded.Messaging.Host.LiteNetLib; + +/// +/// Provides a simple client or host based off of LiteNetLib. +/// +public class LiteNetLibHost : IDisposable, IHost + where TDispatcher : IMessageDispatcher +{ + /// + /// The password necessary to join this host. If it does not match, incoming clients will be rejected. + /// + public string Password { get; set; } + + /// + /// Set to true to accept incoming clients, else reject all clients. + /// + public bool AcceptClients { get; set; } + + /// + /// Event for handling connection requests. + /// + public event EventBasedNetListener.OnConnectionRequest? ConnectionRequestEvent; + + /// + /// Dispatcher for individual messages sent to the client. + /// + public ref TDispatcher Dispatcher => ref _dispatcher; + + /// + /// Exposes the listener with which the manager was created. + /// + public EventBasedNetListener Listener { get; private set; } + + /// + /// The LiteNetLib manager. + /// + public NetManager Manager { get; private set; } + + /// + /// Provides access to first connected peer, useful if client. + /// + public NetPeer FirstPeer => Manager.FirstPeer; + + private TDispatcher _dispatcher; + + /// + /// Provides a simple client or host based off of LiteNetLib. + /// + /// The dispatcher used to send events to your callback handlers. + /// Set to true to accept incoming clients, else reject all requests. + /// The password necessary to join. + public LiteNetLibHost(bool acceptClients, TDispatcher dispatcher, string password = "") + { + _dispatcher = dispatcher; + Password = password; + AcceptClients = acceptClients; + Listener = new EventBasedNetListener(); + Listener.NetworkReceiveEvent += OnNetworkReceive; + Listener.ConnectionRequestEvent += ListenerOnConnectionRequestEvent; + + Manager = new NetManager(Listener); + Manager.UnsyncedEvents = true; + Manager.AutoRecycle = true; + } + + /// + ~LiteNetLibHost() => Dispose(); + + /// + public void Dispose() + { + Manager.Stop(); + GC.SuppressFinalize(this); + } + + private void ListenerOnConnectionRequestEvent(ConnectionRequest request) + { + if (ConnectionRequestEvent != null) + { + ConnectionRequestEvent(request); + return; + } + + if (AcceptClients) + request.AcceptIfKey(Password); + else + request.Reject(); + } + + + /// + public void SendFirstPeer(Span data) + { + FirstPeer.Send(data, DeliveryMethod.ReliableOrdered); + } + + /// + public void SendToAll(Span data) + { + // TODO: This could be better optimised by sending a PR with Span overload in SendToAll + var peers = Manager.ConnectedPeerList; + for (int x = 0; x < peers.Count; x++) + peers[x].Send(data, DeliveryMethod.ReliableOrdered); + } + + // On each message received. +#if NET5_0_OR_GREATER + [SkipLocalsInit] +#endif + private void OnNetworkReceive(NetPeer peer, NetPacketReader reader, byte channel, DeliveryMethod deliveryMethod) + { + var data = AsSpanFast(reader.RawData, reader.Position, reader.AvailableBytes); + var state = new LiteNetLibState(peer, reader, channel, deliveryMethod); + Dispatcher.Dispatch(data, ref state); + } + + /// + /// Provides zero overhead unsafe array to span conversion. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Span AsSpanFast(byte[] data, int start, int length) + { +#if NET5_0_OR_GREATER + return MemoryMarshal.CreateSpan(ref Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(data), start), length); +#else + return data.AsSpan(start, length); +#endif + } +} \ No newline at end of file diff --git a/Source/Reloaded.Messaging.Host.LiteNetLib/LiteNetLibState.cs b/Source/Reloaded.Messaging.Host.LiteNetLib/LiteNetLibState.cs new file mode 100644 index 0000000..d63a237 --- /dev/null +++ b/Source/Reloaded.Messaging.Host.LiteNetLib/LiteNetLibState.cs @@ -0,0 +1,46 @@ +using LiteNetLib; + +namespace Reloaded.Messaging.Host.LiteNetLib; + +/// +/// Encapsulates the state of LiteNetLib at a given point in time. +/// +public struct LiteNetLibState +{ + /// + /// The peer from which the message was received. + /// + public NetPeer Peer { get; } + + /// + /// The reader for reading the raw data. + /// Here for completeness, you probably don't need it. + /// + public NetPacketReader Reader { get; } + + /// + /// The channel using which the message was delivered. + /// + public byte Channel { get; } + + /// + /// The method with which the message was delivered. + /// + public DeliveryMethod DeliveryMethod { get; } + + /// + /// Encapsulates the state of LiteNetLib. + /// + /// The peer from which the message was received. + /// The reader for reading the raw data. You probably don't need it. + /// The channel using which the message was delivered. + /// + + public LiteNetLibState(NetPeer peer, NetPacketReader reader, byte channel, DeliveryMethod deliveryMethod) + { + Peer = peer; + Reader = reader; + Channel = channel; + DeliveryMethod = deliveryMethod; + } +} \ No newline at end of file diff --git a/Source/Reloaded.Messaging.Serializer.SystemTextJson/Reloaded.Messaging.Serializer.SystemTextJson.csproj b/Source/Reloaded.Messaging.Host.LiteNetLib/Reloaded.Messaging.Host.LiteNetLib.csproj similarity index 67% rename from Source/Reloaded.Messaging.Serializer.SystemTextJson/Reloaded.Messaging.Serializer.SystemTextJson.csproj rename to Source/Reloaded.Messaging.Host.LiteNetLib/Reloaded.Messaging.Host.LiteNetLib.csproj index 1616a55..fd928d0 100644 --- a/Source/Reloaded.Messaging.Serializer.SystemTextJson/Reloaded.Messaging.Serializer.SystemTextJson.csproj +++ b/Source/Reloaded.Messaging.Host.LiteNetLib/Reloaded.Messaging.Host.LiteNetLib.csproj @@ -1,39 +1,43 @@ - + - netstandard2.0;net5.0;net7.0 + netstandard2.1;netcoreapp3.1;net5.0 + false preview - Reloaded.Messaging.Serializer.SystemTextJson Sewer56 - Sewer56 - Basic Json serialization implementation for Reloaded.Messaging based off of System.Text.Json. - Sewer56 + + Provides support for LiteNetLib as a host for Reloaded.Messaging. LICENSE.md https://github.com/Reloaded-Project/Reloaded.Messaging https://github.com/Reloaded-Project/Reloaded.Messaging true - true + Sewer56 2.0.0 + + true + 1701;1702;NU5104 + true NuGet-Icon.png + enable - + - - - - - - True - \ - True + + True + \ + + + + + diff --git a/Source/Reloaded.Messaging.Interfaces/ICompressor.cs b/Source/Reloaded.Messaging.Interfaces/ICompressor.cs index da76b77..a5084cf 100644 --- a/Source/Reloaded.Messaging.Interfaces/ICompressor.cs +++ b/Source/Reloaded.Messaging.Interfaces/ICompressor.cs @@ -1,19 +1,31 @@ -namespace Reloaded.Messaging.Interfaces; +using System; + +namespace Reloaded.Messaging.Interfaces; /// /// Defines the minimal interface necessary to bootstrap a 3rd party compressor. /// public interface ICompressor { + /// + /// Gets the maximum possible size of a compressed file. + /// + /// The input size. + /// The maximum possible size after compression. + int GetMaxCompressedSize(int inputSize); + /// /// Compresses the provided byte array. /// - /// The data to compress. - byte[] Compress(byte[] data); + /// The data to compress. + /// The data to compress. + /// Number of compressed bytes. + int Compress(Span uncompressedData, Span compressedData); /// /// Decompresses the provided byte array. /// - /// The data to decompress. - byte[] Decompress(byte[] data); + /// The data to decompress. + /// The buffer containing uncompressed data. + void Decompress(Span compressedBuf, Span uncompressedBuf); } \ No newline at end of file diff --git a/Source/Reloaded.Messaging.Interfaces/IHost.cs b/Source/Reloaded.Messaging.Interfaces/IHost.cs new file mode 100644 index 0000000..0ef01fb --- /dev/null +++ b/Source/Reloaded.Messaging.Interfaces/IHost.cs @@ -0,0 +1,25 @@ +using System; +using System.Net; + +namespace Reloaded.Messaging.Interfaces; + +/// +/// Encapsulates an individual host. +/// +public interface IHost where TDispatcher : IMessageDispatcher +{ + /// + /// The message dispatcher owned by this host instance. + /// + public ref TDispatcher Dispatcher { get; } + + /// + /// Sends a message to the first peer (i.e. client to host). + /// + public void SendFirstPeer(Span data); + + /// + /// Sends a message to all connected peers (i.e. host to clients). + /// + public void SendToAll(Span data); +} \ No newline at end of file diff --git a/Source/Reloaded.Messaging.Interfaces/IMessage.cs b/Source/Reloaded.Messaging.Interfaces/IMessage.cs index b644010..6cedf4e 100644 --- a/Source/Reloaded.Messaging.Interfaces/IMessage.cs +++ b/Source/Reloaded.Messaging.Interfaces/IMessage.cs @@ -5,10 +5,12 @@ namespace Reloaded.Messaging.Interfaces; /// /// Common interface shared by individual messages. /// -public interface IMessage : ISerializable where TMessageType : unmanaged +public interface IMessage : ISerializable + where TSerializer : ISerializer + where TCompressor : ICompressor { /// /// Returns the unique message type/id for this message. /// - TMessageType GetMessageType(); + sbyte GetMessageType(); } \ No newline at end of file diff --git a/Source/Reloaded.Messaging.Interfaces/IMessageDispatcher.cs b/Source/Reloaded.Messaging.Interfaces/IMessageDispatcher.cs new file mode 100644 index 0000000..5851ddd --- /dev/null +++ b/Source/Reloaded.Messaging.Interfaces/IMessageDispatcher.cs @@ -0,0 +1,34 @@ +using System; + +namespace Reloaded.Messaging.Interfaces; + +/// +/// Encapsulates the minimal interface required by a message dispatcher. +/// +/// The extra data +public interface IMessageDispatcher +{ + /// + /// Gets a handler that handles a specific message type. + /// + /// The type of message requested. + /// Handler for the specific message. Might be null. + ref IMessageHandlerBase? GetHandlerForType(byte messageType); + + /// + /// Sets a handler for a specific message type. + /// + void AddOrOverrideHandler(IMessageHandlerBase handler); + + /// + /// Removes a handler assigned to a specific message type. + /// + void RemoveHandler(byte messageType); + + /// + /// Given a raw network message, decodes the message and delegates it to an appropriate handling method. + /// + /// Data containing a packed Reloaded.Messaging message. + /// The extra data associated with this request. + void Dispatch(Span data, ref TExtraData extraData); +} diff --git a/Source/Reloaded.Messaging.Interfaces/IMessageHandlerBase.cs b/Source/Reloaded.Messaging.Interfaces/IMessageHandlerBase.cs new file mode 100644 index 0000000..9e6405b --- /dev/null +++ b/Source/Reloaded.Messaging.Interfaces/IMessageHandlerBase.cs @@ -0,0 +1,23 @@ +using System; + +namespace Reloaded.Messaging.Interfaces; + +/// +/// Common interface shared by types that perform direct deserialization and handling of messages. +/// +/// Type of extra data attached to the message. Usually state of network library. +public interface IMessageHandlerBase +{ + /// + /// Returns the unique message type/id for this message. + /// + sbyte GetMessageType(); + + /// + /// Handles a specific incoming message. + /// + /// The raw data, without message header, ready for decompression and deserialization. + /// Expected size after decompression. + /// Extra data. Usually state of networking library used. + void HandleMessage(Span data, int decompressedSize, ref TExtraData extraData); +} \ No newline at end of file diff --git a/Source/Reloaded.Messaging.Interfaces/ISerializer.cs b/Source/Reloaded.Messaging.Interfaces/ISerializer.cs index 89b8c97..56b369e 100644 --- a/Source/Reloaded.Messaging.Interfaces/ISerializer.cs +++ b/Source/Reloaded.Messaging.Interfaces/ISerializer.cs @@ -1,21 +1,24 @@ -namespace Reloaded.Messaging.Interfaces; +using System; +using System.Buffers; + +namespace Reloaded.Messaging.Interfaces; /// /// Defines the minimal interface necessary to bootstrap a 3rd party serializer. /// -public interface ISerializer +public interface ISerializer { /// /// Deserializes the provided byte array into a concrete type. /// - /// The type of the structure to deserialize. /// The data to deserialize. - TStruct Deserialize(byte[] serialized); + TStruct Deserialize(Span serialized); /// /// Serializes the provided item into a byte array. /// /// The item to serialize to bytes. + /// The writer into which the serialized message should be written to. /// Serialized item. - byte[] Serialize(ref TStruct item); + void Serialize(ref TStruct item, IBufferWriter writer); } \ No newline at end of file diff --git a/Source/Reloaded.Messaging.Interfaces/Message/ISerializable.cs b/Source/Reloaded.Messaging.Interfaces/Message/ISerializable.cs index 5d76aa5..b08db67 100644 --- a/Source/Reloaded.Messaging.Interfaces/Message/ISerializable.cs +++ b/Source/Reloaded.Messaging.Interfaces/Message/ISerializable.cs @@ -3,15 +3,16 @@ /// /// An interface that provides serialization/deserialization and compression/decompression support for. /// -public interface ISerializable +public interface ISerializable where TSerializer : ISerializer + where TCompressor : ICompressor { /// /// Returns the serializer for this specific type. /// - ISerializer GetSerializer(); + TSerializer GetSerializer(); /// /// Returns the compressor for this specific type. /// - ICompressor GetCompressor(); + TCompressor? GetCompressor(); } \ No newline at end of file diff --git a/Source/Reloaded.Messaging.Interfaces/Reloaded.Messaging.Interfaces.csproj b/Source/Reloaded.Messaging.Interfaces/Reloaded.Messaging.Interfaces.csproj index 43876b9..2d56eac 100644 --- a/Source/Reloaded.Messaging.Interfaces/Reloaded.Messaging.Interfaces.csproj +++ b/Source/Reloaded.Messaging.Interfaces/Reloaded.Messaging.Interfaces.csproj @@ -1,7 +1,7 @@  - netstandard2.0 + netstandard2.1;netstandard2.0;netcoreapp3.1;net5.0 preview Contains all of the interfaces (and some extension functionality) used by the base Reloaded.Messaging library. This package exists to allow you to use various features of the library, such as serializers without the need to import the dependencies of the base package. @@ -12,8 +12,15 @@ This package exists to allow you to use various features of the library, such as true NuGet-Icon.png 2.0.0 + $(DefineConstants);USE_NATIVE_SPAN_API + true + enable + + + + True diff --git a/Source/Reloaded.Messaging.Interfaces/Serializable.cs b/Source/Reloaded.Messaging.Interfaces/Serializable.cs deleted file mode 100644 index e21a21a..0000000 --- a/Source/Reloaded.Messaging.Interfaces/Serializable.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System.Runtime.CompilerServices; -using Reloaded.Messaging.Interfaces.Message; - -namespace Reloaded.Messaging.Interfaces; - -/// -/// An extension class providing serialization support to implementers of . -/// -public static class Serializable -{ - /// - /// Serializes and compresses the current instance of the class or struct - /// using the serializer and compressor defined by the . - /// - public static byte[] Serialize(this TSerializable serializable) where TSerializable : ISerializable - { - var serializer = serializable.GetSerializer(); - var compressor = serializable.GetCompressor(); - - byte[] serialized = serializer.Serialize(ref serializable); - if (compressor != null) - return compressor.Compress(serialized); - - return serialized; - } - - /// - /// Decompresses and deserializes the current instance of the class or struct using the - /// serializer and compressor defined by the . - /// - public static ISerializable Deserialize(this TType serializable, byte[] bytes) where TType : ISerializable - { - var compressor = serializable.GetCompressor(); - var serializer = serializable.GetSerializer(); - - byte[] decompressed = bytes; - if (compressor != null) - decompressed = compressor.Decompress(bytes); - - return serializer.Deserialize(decompressed); - } - - /// - /// Decompresses and deserializes the current instance of the class or struct using the - /// serializer and compressor defined by the . - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static ISerializable Deserialize(byte[] bytes) where TType : ISerializable, new() - { - var serializable = new TType(); - return Deserialize(serializable, bytes); - } -} \ No newline at end of file diff --git a/Source/Reloaded.Messaging.Interfaces/Utilities/NullCompressor.cs b/Source/Reloaded.Messaging.Interfaces/Utilities/NullCompressor.cs new file mode 100644 index 0000000..5baa4b1 --- /dev/null +++ b/Source/Reloaded.Messaging.Interfaces/Utilities/NullCompressor.cs @@ -0,0 +1,29 @@ +using System; + +namespace Reloaded.Messaging.Interfaces.Utilities; + +/// +/// Dummy compressor that performs no compression. +/// Use me when specifying TCompressor and return null in structures. +/// +public class NullCompressor : ICompressor +{ + /// + public int GetMaxCompressedSize(int inputSize) + { + return inputSize; + } + + /// + public int Compress(Span uncompressedData, Span compressedData) + { + uncompressedData.CopyTo(compressedData); + return uncompressedData.Length; + } + + /// + public void Decompress(Span compressedBuf, Span uncompressedBuf) + { + compressedBuf.CopyTo(uncompressedBuf); + } +} \ No newline at end of file diff --git a/Source/Reloaded.Messaging.Serializer.MessagePack/MessagePackSerializer.cs b/Source/Reloaded.Messaging.Serializer.MessagePack/MessagePackSerializer.cs new file mode 100644 index 0000000..1a3f886 --- /dev/null +++ b/Source/Reloaded.Messaging.Serializer.MessagePack/MessagePackSerializer.cs @@ -0,0 +1,60 @@ +using System; +using System.Buffers; +using System.IO; +using System.Runtime.CompilerServices; +using MessagePack; +using MessagePack.Resolvers; +using Reloaded.Messaging.Interfaces; + +namespace Reloaded.Messaging.Serializer.MessagePack; + +/// +/// Serializer that uses MessagePack. +/// +public struct MessagePackSerializer : ISerializer +{ + /// + /// Options for the MessagePack serializer. + /// + public MessagePackSerializerOptions SerializerOptions { get; private set; } = MessagePackSerializerOptions.Standard; + + /// + /// Creates a new instance of the MessagePack serializer. + /// + public MessagePackSerializer() + { + SerializerOptions = SerializerOptions.WithResolver(ContractlessStandardResolver.Instance); + } + + /// + /// Creates a new instance of the MessagePack serializer. + /// + /// + /// Custom resolver to pass to MessagePack, default instance uses "Contractless Resolver". + /// + public MessagePackSerializer(IFormatterResolver? resolver = null) + { + SerializerOptions = SerializerOptions.WithResolver(resolver ?? ContractlessStandardResolver.Instance); + } + + /// +#if NET5_0_OR_GREATER + [SkipLocalsInit] +#endif + public unsafe TStruct Deserialize(Span serialized) + { + fixed (byte* dataPtr = &serialized[0]) + { + // We have to tank potential heap allocation here. + // Hoping JIT escape analysis is smart enough not to heap allocate this one. + var manager = new UnmanagedMemoryManager(dataPtr, serialized.Length); + return MessagePackSerializer.Deserialize(manager.Memory, SerializerOptions); + } + } + + /// + public void Serialize(ref TStruct item, IBufferWriter writer) + { + MessagePackSerializer.Serialize(writer, item, SerializerOptions); + } +} \ No newline at end of file diff --git a/Source/Reloaded.Messaging.Serializer.MessagePack/MsgPackSerializer.cs b/Source/Reloaded.Messaging.Serializer.MessagePack/MsgPackSerializer.cs deleted file mode 100644 index ce095db..0000000 --- a/Source/Reloaded.Messaging.Serializer.MessagePack/MsgPackSerializer.cs +++ /dev/null @@ -1,40 +0,0 @@ -using MessagePack; -using MessagePack.Resolvers; -using Reloaded.Messaging.Interfaces; - -namespace Reloaded.Messaging.Serializer.MessagePack; - -/// -/// Serializer that uses MessagePack. -/// -public class MsgPackSerializer : ISerializer -{ - /// - /// Options for the MessagePack serializer. - /// - public MessagePackSerializerOptions SerializerOptions { get; private set; } = MessagePackSerializerOptions.Standard; - - /// - /// Creates a new instance of the MessagePack serializer. - /// - /// - /// Custom resolver to pass to MessagePack, default instance uses "Contractless Resolver". - /// - public MsgPackSerializer(IFormatterResolver resolver = null) - { - SerializerOptions = SerializerOptions.WithResolver(resolver ?? ContractlessStandardResolver.Instance); - } - - /// - public TStruct Deserialize(byte[] serialized) - { - return MessagePackSerializer.Deserialize(serialized, SerializerOptions); - } - - - /// - public byte[] Serialize(ref TStruct item) - { - return MessagePackSerializer.Serialize(item, SerializerOptions); - } -} \ No newline at end of file diff --git a/Source/Reloaded.Messaging.Serializer.MessagePack/Reloaded.Messaging.Serializer.MessagePack.csproj b/Source/Reloaded.Messaging.Serializer.MessagePack/Reloaded.Messaging.Serializer.MessagePack.csproj index 8b1a02d..9ff58b8 100644 --- a/Source/Reloaded.Messaging.Serializer.MessagePack/Reloaded.Messaging.Serializer.MessagePack.csproj +++ b/Source/Reloaded.Messaging.Serializer.MessagePack/Reloaded.Messaging.Serializer.MessagePack.csproj @@ -1,9 +1,8 @@  - netstandard2.1;netstandard2.0 + netstandard2.1;netstandard2.0;netcoreapp3.1;net5.0 preview - Reloaded.Messaging.Serializer.MessagePack Sewer56 Sewer56 Basic MessagePack serialization implementation for Reloaded.Messaging based off of MessagePack-CSharp. @@ -15,6 +14,8 @@ 2.0.0 true NuGet-Icon.png + true + enable diff --git a/Source/Reloaded.Messaging.Serializer.MessagePack/UnmanagedMemoryManager.cs b/Source/Reloaded.Messaging.Serializer.MessagePack/UnmanagedMemoryManager.cs new file mode 100644 index 0000000..88e6958 --- /dev/null +++ b/Source/Reloaded.Messaging.Serializer.MessagePack/UnmanagedMemoryManager.cs @@ -0,0 +1,52 @@ +using System; +using System.Buffers; + +namespace Reloaded.Messaging.Serializer.MessagePack; + +/// +/// A MemoryManager over a raw pointer, used for conversion to for the serializers that require it. +/// Pointers passed to this method are expected to be externally pinned. +/// +public sealed unsafe class UnmanagedMemoryManager : MemoryManager where T : unmanaged +{ + private T* _pointer; + private int _length; + + /// + /// Creates a UnmanagedMemoryManager from pointer and size. + /// + public UnmanagedMemoryManager(T* pointer, int length) + { + _pointer = pointer; + _length = length; + } + + /// + /// Updates the values behind the current instance. + /// + public void Update(T* pointer, int length) + { + _pointer = pointer; + _length = length; + } + + /// + /// Obtains a span that represents the region. + /// + public override Span GetSpan() => new(_pointer, _length); + + /// + /// Returns pointer representing the data [no pin occurs]. + /// + public override MemoryHandle Pin(int elementIndex = 0) => new(_pointer + elementIndex); + + /// + /// Has no effect. + /// + public override void Unpin() { } + + /// + /// Releases all resources associated with this object + /// + protected override void Dispose(bool disposing) { } +} \ No newline at end of file diff --git a/Source/Reloaded.Messaging.Serializer.NewtonsoftJson/NewtonsoftJsonSerializer.cs b/Source/Reloaded.Messaging.Serializer.NewtonsoftJson/NewtonsoftJsonSerializer.cs deleted file mode 100644 index 6e9c73e..0000000 --- a/Source/Reloaded.Messaging.Serializer.NewtonsoftJson/NewtonsoftJsonSerializer.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System; -using System.Runtime.Serialization; -using System.Text; -using Newtonsoft.Json; -using Reloaded.Messaging.Interfaces; - -namespace Reloaded.Messaging.Serializer.NewtonsoftJson; - -/// -/// -/// -public class NewtonsoftJsonSerializer : ISerializer -{ - /// - /// Serialization options. - /// - public JsonSerializerSettings Options { get; private set; } - - /// - /// Creates the System.Text.Json based serializer. - /// - /// Options to use for serialization/deserialization. - public NewtonsoftJsonSerializer(JsonSerializerSettings serializerOptions) - { - Options = serializerOptions; - } - - /// - public TStruct Deserialize(byte[] serialized) - { - return JsonConvert.DeserializeObject(Encoding.UTF8.GetString(serialized), Options); - } - - /// - public byte[] Serialize(ref TStruct item) - { - return Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(item, Options)); - } -} \ No newline at end of file diff --git a/Source/Reloaded.Messaging.Serializer.ReloadedMemory/Reloaded.Messaging.Serializer.ReloadedMemory.csproj b/Source/Reloaded.Messaging.Serializer.ReloadedMemory/Reloaded.Messaging.Serializer.ReloadedMemory.csproj index 3a1f75b..51b1230 100644 --- a/Source/Reloaded.Messaging.Serializer.ReloadedMemory/Reloaded.Messaging.Serializer.ReloadedMemory.csproj +++ b/Source/Reloaded.Messaging.Serializer.ReloadedMemory/Reloaded.Messaging.Serializer.ReloadedMemory.csproj @@ -1,11 +1,11 @@  - netstandard2.1;netstandard2.0 + netstandard2.1;netstandard2.0;netcoreapp3.1;net5.0 preview Sewer56 Reloaded.Messaging.Serializer.ReloadedMemory - Basic Reloaded.Memory based serialization implementation for Reloaded.Messaging that converts structs to their raw byte representation and back. + Basic Reloaded.Memory based serialization implementation for Reloaded.Messaging, for those happy with manual serialization or converting to raw bytes. LICENSE.md https://github.com/Reloaded-Project/Reloaded.Messaging https://github.com/Reloaded-Project/Reloaded.Messaging @@ -13,10 +13,11 @@ 2.0.0 true NuGet-Icon.png + true - + diff --git a/Source/Reloaded.Messaging.Serializer.ReloadedMemory/ReloadedMemorySerializer.cs b/Source/Reloaded.Messaging.Serializer.ReloadedMemory/ReloadedMemorySerializer.cs deleted file mode 100644 index 25b8c59..0000000 --- a/Source/Reloaded.Messaging.Serializer.ReloadedMemory/ReloadedMemorySerializer.cs +++ /dev/null @@ -1,42 +0,0 @@ -using Reloaded.Memory; -using Reloaded.Messaging.Interfaces; - -namespace Reloaded.Messaging.Serializer.ReloadedMemory; - -/// -/// Serializes messages using raw byte conversion with Reloaded.Memory. -/// -public class ReloadedMemorySerializer : ISerializer -{ - /// - /// Marshals structures if set to true however is significantly slower. - /// Note: Marshalling also allows you to serialize Classes with [StructLayout] attribute. - /// - public bool MarshalValues { get; private set; } - - /// - /// Creates the Reloaded.Memory based serializer. - /// - /// - /// Marshals structures if set to true however is significantly slower. - /// Note: Marshalling also allows you to serialize Classes with [StructLayout] attribute. - /// - public ReloadedMemorySerializer(bool marshalValues) - { - MarshalValues = marshalValues; - } - - /// - public TStruct Deserialize(byte[] serialized) - { - Struct.FromArray(serialized, out TStruct value, MarshalValues, 0); - return value; - } - - /// - public byte[] Serialize(ref TStruct item) - { - byte[] bytes = Struct.GetBytes(ref item, MarshalValues); - return bytes; - } -} \ No newline at end of file diff --git a/Source/Reloaded.Messaging.Serializer.ReloadedMemory/UnmanagedReloadedMemorySerializer.cs b/Source/Reloaded.Messaging.Serializer.ReloadedMemory/UnmanagedReloadedMemorySerializer.cs new file mode 100644 index 0000000..cf98303 --- /dev/null +++ b/Source/Reloaded.Messaging.Serializer.ReloadedMemory/UnmanagedReloadedMemorySerializer.cs @@ -0,0 +1,37 @@ +using System; +using System.Buffers; +using Reloaded.Memory; +using Reloaded.Messaging.Interfaces; + +#if NET5_0_OR_GREATER +using System.Runtime.CompilerServices; +#endif + +namespace Reloaded.Messaging.Serializer.ReloadedMemory; + +/// +/// Serializes messages using raw byte conversion with Reloaded.Memory. +/// +public unsafe struct UnmanagedReloadedMemorySerializer : ISerializer where TStruct : unmanaged +{ + /// +#if NET5_0_OR_GREATER + [SkipLocalsInit] +#endif + public TStruct Deserialize(Span serialized) + { + Struct.FromArray(serialized, out TStruct value); + return value; + } + + /// +#if NET5_0_OR_GREATER + [SkipLocalsInit] +#endif + public void Serialize(ref TStruct item, IBufferWriter writer) + { + var span = writer.GetSpan(sizeof(TStruct)); + Struct.GetBytes(ref item, span); + writer.Advance(sizeof(TStruct)); + } +} \ No newline at end of file diff --git a/Source/Reloaded.Messaging.Serializer.SystemTextJson/SystemTextJsonSerializer.cs b/Source/Reloaded.Messaging.Serializer.SystemTextJson/SystemTextJsonSerializer.cs deleted file mode 100644 index 992cdba..0000000 --- a/Source/Reloaded.Messaging.Serializer.SystemTextJson/SystemTextJsonSerializer.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System; -using System.Text.Json; -using Reloaded.Messaging.Interfaces; - -namespace Reloaded.Messaging.Serializer.SystemTextJson; - -/// -public class SystemTextJsonSerializer : ISerializer -{ - /// - /// Serialization options. - /// - public JsonSerializerOptions Options { get; private set; } - - /// - /// Creates the System.Text.Json based serializer. - /// - /// Options to use for serialization/deserialization. - public SystemTextJsonSerializer(JsonSerializerOptions serializerOptions) - { - Options = serializerOptions; - } - - /// - public TStruct Deserialize(byte[] serialized) - { - return JsonSerializer.Deserialize(serialized, Options); - } - - /// - public byte[] Serialize(ref TStruct item) - { - return JsonSerializer.SerializeToUtf8Bytes(item, Options); - } -} \ No newline at end of file diff --git a/Source/Reloaded.Messaging.Tests/Init/TestingHosts.cs b/Source/Reloaded.Messaging.Tests/Init/TestingHosts.cs deleted file mode 100644 index 5eb9c43..0000000 --- a/Source/Reloaded.Messaging.Tests/Init/TestingHosts.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System; -using System.Net; - -namespace Reloaded.Messaging.Tests.Init; - -public class TestingHosts : IDisposable -{ - private const string DefaultPassword = "CutenessIsJustice"; - public SimpleHost SimpleServer; - public SimpleHost SimpleClient; - - public TestingHosts() - { - SimpleServer = new SimpleHost(true, DefaultPassword); - SimpleClient = new SimpleHost(false, DefaultPassword); - - SimpleServer.NetManager.Start(IPAddress.Loopback, IPAddress.IPv6Loopback, 0); - SimpleClient.NetManager.Start(IPAddress.Loopback, IPAddress.IPv6Loopback, 0); - SimpleClient.NetManager.Connect(new IPEndPoint(IPAddress.Loopback, SimpleServer.NetManager.LocalPort), DefaultPassword); - -#if DEBUG - SimpleServer.NetManager.DisconnectTimeout = int.MaxValue; - SimpleClient.NetManager.DisconnectTimeout = int.MaxValue; -#endif - } - - public void Dispose() - { - SimpleServer?.Dispose(); - SimpleClient?.Dispose(); - } -} \ No newline at end of file diff --git a/Source/Reloaded.Messaging.Tests/LiteNetLibHostTests.cs b/Source/Reloaded.Messaging.Tests/LiteNetLibHostTests.cs new file mode 100644 index 0000000..013bf6a --- /dev/null +++ b/Source/Reloaded.Messaging.Tests/LiteNetLibHostTests.cs @@ -0,0 +1,72 @@ +using System; +using System.Net; +using LiteNetLib; +using System.Threading.Tasks; +using Reloaded.Messaging.Host.LiteNetLib; +using Reloaded.Messaging.Messages; +using Reloaded.Messaging.Tests.Messages; +using Reloaded.Messaging.Utilities; +using Xunit; + +namespace Reloaded.Messaging.Tests; + +public class TestingHosts : IDisposable +{ + private const string DefaultPassword = "SwagSwagSwag"; + public LiteNetLibHost> Server; + public LiteNetLibHost> Client; + + public TestingHosts() + { + Server = new LiteNetLibHost>(true, new MessageDispatcher(), DefaultPassword); + Client = new LiteNetLibHost>(true, new MessageDispatcher(), DefaultPassword); + + Server.Manager.Start(IPAddress.Loopback, IPAddress.IPv6Loopback, 0); + Client.Manager.Start(IPAddress.Loopback, IPAddress.IPv6Loopback, 0); + Client.Manager.Connect(new IPEndPoint(IPAddress.Loopback, Server.Manager.LocalPort), DefaultPassword); + +#if DEBUG + Server.Manager.DisconnectTimeout = int.MaxValue; + Client.Manager.DisconnectTimeout = int.MaxValue; +#endif + } + + public void Dispose() + { + Server?.Dispose(); + Client?.Dispose(); + } + + [Fact(Timeout = 5000)] + public async Task SendAndReceiveMessage() + { + // Arrange/Setup Host + var messageHandler = new LiteNetLibMessageHandler(); + messageHandler.Received.AddToDispatcher(messageHandler, ref Server.Dispatcher); + + // Send sample message. + var sample = new Vector3(0.0f, 1.0f, 2.0f); + using var serialized = sample.Serialize(ref sample); + Client.SendFirstPeer(serialized.Span); + + // Wait for response. + while (messageHandler.Received.Equals(default)) + await Task.Delay(16); + + Assert.Equal(sample, messageHandler.Received); + Assert.NotNull(messageHandler.State.Peer); + Assert.NotNull(messageHandler.State.Reader); + } + + public class LiteNetLibMessageHandler : IMsgRefAction + { + public Vector3 Received; + public LiteNetLibState State; + + public void OnMessageReceive(ref Vector3 received, ref LiteNetLibState data) + { + Received = received; + State = data; + } + } +} \ No newline at end of file diff --git a/Source/Reloaded.Messaging.Tests/MessageCreationTests.cs b/Source/Reloaded.Messaging.Tests/MessageCreationTests.cs new file mode 100644 index 0000000..108d03d --- /dev/null +++ b/Source/Reloaded.Messaging.Tests/MessageCreationTests.cs @@ -0,0 +1,133 @@ +using Reloaded.Messaging.Messages; +using Reloaded.Messaging.Tests.Messages; +using System; +using Reloaded.Messaging.Compressor.ZStandard; +using Reloaded.Messaging.Extras.Runtime; +using Reloaded.Messaging.Interfaces; +using Xunit; +using Reloaded.Messaging.Interfaces.Utilities; +using Reloaded.Messaging.Serializer.MessagePack; +using Reloaded.Messaging.Serializer.ReloadedMemory; + +namespace Reloaded.Messaging.Tests; + +public class MessageCreationTests +{ + #region Baseline Tests + [Fact] + public unsafe void CreateMessage_Baseline_WithReloadedMemoryNoCompression() + { + // Arrange + var structure = new Vector3(0.5f, 1.0f, 2.0f); + CreateMessage_Serializer_Common, NullCompressor>(ref structure); + } + + [Fact] + public unsafe void CreateMessage_Baseline_WithReloadedMemoryDummyCompression() + { + // Arrange + var structure = new Vector3ReloadedMemoryDummyCompression(0.5f, 1.0f, 2.0f); + using var message = MessageWriter, NullCompressor>.Serialize(ref structure); + + // Act & Assert + var sizeEqual = (sizeof(Vector3) + HeaderReader.CompressedHeaderSize) == message.Span.Length; + Assert.True(sizeEqual, "Size after packing does not match expected size."); + + HeaderReader.ReadHeader(message.Span, out var messageType, out var compressedSize, out var headerSize); + Assert.True(structure.GetMessageType().Equals(messageType), "Invalid message type."); + Assert.True(compressedSize == sizeof(Vector3ReloadedMemoryDummyCompression), "Compressed size should return invalid if no compression is used."); + + var deserialize = new MessageReader, NullCompressor>(in structure); + var deserialized = deserialize.Deserialize(message.Span.Slice(headerSize), compressedSize); + Assert.True(structure.Equals(deserialized), "Vectors should be equal after deserialize."); + } + #endregion + + #region Serializer Tests + [Fact] + public unsafe void CreateMessage_WithMessagePack() + { + // Arrange + var structure = new Vector3MessagePackNoCompression(0.5f, 1.0f, 2.0f); + CreateMessage_Serializer_Common, NullCompressor>(ref structure); + } + + [Fact] + public unsafe void CreateMessage_WithSystemTextJson() + { + // Arrange + var structure = new Vector3SystemTextJson(0.5f, 1.0f, 2.0f); + CreateMessage_Serializer_Common, NullCompressor>(ref structure); + } + +#if NET6_0_OR_GREATER + [Fact] + public unsafe void CreateMessage_WithSystemTextJsonSourceGen() + { + // Arrange + var structure = new Vector3SystemTextJsonSourceGenerated(0.5f, 1.0f, 2.0f); + CreateMessage_Serializer_Common, NullCompressor>(ref structure); + } +#endif + + #endregion + + #region Compressor Tests + + [Fact] + public unsafe void CreateMessage_WithZStandard() + { + // Arrange + var structure = new Vector3ZStandard(0.5f, 1.0f, 2.0f); + CreateMessage_Compressor_Common, ZStandardCompressor>(ref structure); + } + + [Fact] + public unsafe void CreateMessage_WithBrotli() + { + // Arrange + var structure = new Vector3Brotli(0.5f, 1.0f, 2.0f); + CreateMessage_Compressor_Common, BrotliCompressor>(ref structure); + } + + #endregion + + /// + /// Common function for testing serializers. + /// + private unsafe void CreateMessage_Serializer_Common(ref TStruct value) where TStruct : IMessage, IEquatable, new() + where TSerializer : ISerializer + where TCompressor : ICompressor + { + // Arrange + using var message = MessageWriter.Serialize(ref value); + + // Act & Assert + HeaderReader.ReadHeader(message.Span, out var messageType, out var sizeAfterDecompression, out var headerSize); + Assert.True(value.GetMessageType().Equals(messageType), "Invalid message type."); + Assert.True(sizeAfterDecompression == -1, "Compressed size should return invalid if no compression is used."); + + var deserialize = new MessageReader(in value); + var deserialized = deserialize.Deserialize(message.Span.Slice(headerSize), -1); + Assert.True(value.Equals(deserialized), "Values should be equal after deserialize."); + } + + /// + /// Common function for testing serializers. + /// + private unsafe void CreateMessage_Compressor_Common(ref TStruct value) where TStruct : IMessage, new() + where TSerializer : ISerializer + where TCompressor : ICompressor + { + // Arrange + using var message = MessageWriter.Serialize(ref value); + + // Act & Assert + HeaderReader.ReadHeader(message.Span, out var messageType, out var sizeAfterDecompression, out var headerSize); + Assert.True(value.GetMessageType().Equals(messageType), "Invalid message type."); + + var deserialize = new MessageReader(in value); + var deserialized = deserialize.Deserialize(message.Span.Slice(headerSize), sizeAfterDecompression); + Assert.True(value.Equals(deserialized), "Values should be equal after deserialize."); + } +} \ No newline at end of file diff --git a/Source/Reloaded.Messaging.Tests/MessageDispatcherTests.cs b/Source/Reloaded.Messaging.Tests/MessageDispatcherTests.cs new file mode 100644 index 0000000..d4cb75b --- /dev/null +++ b/Source/Reloaded.Messaging.Tests/MessageDispatcherTests.cs @@ -0,0 +1,88 @@ +using Reloaded.Messaging.Messages; +using Reloaded.Messaging.Tests.Messages; +using Reloaded.Messaging.Utilities; +using Xunit; + +namespace Reloaded.Messaging.Tests; + +/// +/// Provides various tests for the high end message handler. +/// +public class MessageDispatcherTests +{ + [Fact] + public void Dispatch_GetHandler_WithNoHandler_ReturnsNull() + { + // Arrange + var dispatcher = new MessageDispatcher(); + + // Check all slots + for (int x = 0; x < sbyte.MaxValue; x++) + Assert.Null(dispatcher.GetHandlerForType((byte)x)); + } + + [Fact] + public void Dispatch_WithHandler_ReturnNotNull() + { + // Arrange + var dispatcher = new MessageDispatcher(); + var sample = new Vector3(0.5f, 1.0f, 2.0f); + + // Add to dispatcher. + var receiveAction = new Vector3ReceiveAction(); + sample.AddToDispatcher(receiveAction, ref dispatcher); + + // Pack message. + Assert.NotNull(dispatcher.GetHandlerForType((byte)sample.GetMessageType())); + } + + [Fact] + public void Dispatch_WithRemovedHandler_ReturnNull() + { + // Arrange + var dispatcher = new MessageDispatcher(); + var sample = new Vector3(0.5f, 1.0f, 2.0f); + + // Add to dispatcher. + var receiveAction = new Vector3ReceiveAction(); + sample.AddToDispatcher(receiveAction, ref dispatcher); + + // Check for presence, then after removal. + Assert.NotNull(dispatcher.GetHandlerForType((byte)sample.GetMessageType())); + dispatcher.RemoveHandler((byte)sample.GetMessageType()); + Assert.Null(dispatcher.GetHandlerForType((byte)sample.GetMessageType())); + } + + [Fact] + public void Dispatch_WithHandler_ReceivesMessage() + { + // Arrange + var dispatcher = new MessageDispatcher(); + var sample = new Vector3(0.5f, 1.0f, 2.0f); + + // Add to dispatcher. + var receiveAction = new Vector3ReceiveAction(); + sample.AddToDispatcher(receiveAction, ref dispatcher); + + // Pack message. + using var serialized = sample.Serialize(ref sample); + var extraData = 42; + dispatcher.Dispatch(serialized.Span, ref extraData); + + // Check message was received. + Assert.Equal(extraData, receiveAction.ExtraData); + Assert.Equal(sample, receiveAction.Received); + } + + public class Vector3ReceiveAction : IMsgRefAction + { + public Vector3 Received; + public int ExtraData; + + public void OnMessageReceive(ref Vector3 received, ref int data) + { + Received = received; + ExtraData = data; + } + } +} \ No newline at end of file diff --git a/Source/Reloaded.Messaging.Tests/MessageType.cs b/Source/Reloaded.Messaging.Tests/MessageType.cs deleted file mode 100644 index 2d16aef..0000000 --- a/Source/Reloaded.Messaging.Tests/MessageType.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Reloaded.Messaging.Tests; - -public enum MessageType : byte -{ - String, - Vector3 -} \ No newline at end of file diff --git a/Source/Reloaded.Messaging.Tests/Messages/MessageType.cs b/Source/Reloaded.Messaging.Tests/Messages/MessageType.cs new file mode 100644 index 0000000..c4d7ebb --- /dev/null +++ b/Source/Reloaded.Messaging.Tests/Messages/MessageType.cs @@ -0,0 +1,11 @@ +namespace Reloaded.Messaging.Tests.Messages; + +public enum MessageType : byte +{ + Vector3, + Vector3ReloadedMemoryDummyCompression, + Vector3MessagePackNoCompression, + Vector3ZStandard, + Vector3SystemTextJson, + Vector3Brotli +} \ No newline at end of file diff --git a/Source/Reloaded.Messaging.Tests/Struct/Vector3.cs b/Source/Reloaded.Messaging.Tests/Messages/Vector3.cs similarity index 52% rename from Source/Reloaded.Messaging.Tests/Struct/Vector3.cs rename to Source/Reloaded.Messaging.Tests/Messages/Vector3.cs index 4d9df8f..b80136f 100644 --- a/Source/Reloaded.Messaging.Tests/Struct/Vector3.cs +++ b/Source/Reloaded.Messaging.Tests/Messages/Vector3.cs @@ -1,18 +1,19 @@ using Reloaded.Messaging.Interfaces; -using Reloaded.Messaging.Messages; -using Reloaded.Messaging.Serializer.MessagePack; +using Reloaded.Messaging.Serializer.ReloadedMemory; +using System; +using Reloaded.Messaging.Interfaces.Utilities; -namespace Reloaded.Messaging.Tests.Struct; +namespace Reloaded.Messaging.Tests.Messages; -public struct Vector3 : IMessage +public struct Vector3 : IMessage, NullCompressor>, IEquatable { - public MessageType GetMessageType() => MessageType.Vector3; - public ISerializer GetSerializer() => new MsgPackSerializer(); - public ICompressor GetCompressor() => null; + public sbyte GetMessageType() => (sbyte)MessageType.Vector3; + public UnmanagedReloadedMemorySerializer GetSerializer() => new(); + public NullCompressor? GetCompressor() => null; - public float X { get; set; } - public float Y { get; set; } - public float Z { get; set; } + public float X; + public float Y; + public float Z; public Vector3(float x, float y, float z) { @@ -21,13 +22,13 @@ public Vector3(float x, float y, float z) Z = z; } - /* Auto-implemented by R# */ - private bool Equals(Vector3 other) + // Auto-implemented by R# + public bool Equals(Vector3 other) { return X.Equals(other.X) && Y.Equals(other.Y) && Z.Equals(other.Z); } - public override bool Equals(object obj) + public override bool Equals(object? obj) { if (ReferenceEquals(null, obj)) return false; @@ -48,5 +49,4 @@ public override int GetHashCode() return hashCode; } } - } \ No newline at end of file diff --git a/Source/Reloaded.Messaging.Tests/Messages/Vector3Brotli.cs b/Source/Reloaded.Messaging.Tests/Messages/Vector3Brotli.cs new file mode 100644 index 0000000..214e543 --- /dev/null +++ b/Source/Reloaded.Messaging.Tests/Messages/Vector3Brotli.cs @@ -0,0 +1,53 @@ +using Reloaded.Messaging.Interfaces; +using System; +using Reloaded.Messaging.Compressor.ZStandard; +using Reloaded.Messaging.Extras.Runtime; +using Reloaded.Messaging.Serializer.ReloadedMemory; + +namespace Reloaded.Messaging.Tests.Messages; + +public struct Vector3Brotli : IMessage, BrotliCompressor>, IEquatable +{ + public sbyte GetMessageType() => (sbyte)MessageType.Vector3Brotli; + public UnmanagedReloadedMemorySerializer GetSerializer() => new(); + public BrotliCompressor GetCompressor() => new(); + + public float X { get; set; } + public float Y { get; set; } + public float Z { get; set; } + + public Vector3Brotli(float x, float y, float z) + { + X = x; + Y = y; + Z = z; + } + + // Auto-implemented by R# + public bool Equals(Vector3Brotli other) + { + return X.Equals(other.X) && Y.Equals(other.Y) && Z.Equals(other.Z); + } + + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) + return false; + + if (obj.GetType() != this.GetType()) + return false; + + return Equals((Vector3Brotli)obj); + } + + public override int GetHashCode() + { + unchecked + { + var hashCode = X.GetHashCode(); + hashCode = (hashCode * 397) ^ Y.GetHashCode(); + hashCode = (hashCode * 397) ^ Z.GetHashCode(); + return hashCode; + } + } +} \ No newline at end of file diff --git a/Source/Reloaded.Messaging.Tests/Messages/Vector3MessagePackNoCompression.cs b/Source/Reloaded.Messaging.Tests/Messages/Vector3MessagePackNoCompression.cs new file mode 100644 index 0000000..cd2d595 --- /dev/null +++ b/Source/Reloaded.Messaging.Tests/Messages/Vector3MessagePackNoCompression.cs @@ -0,0 +1,52 @@ +using Reloaded.Messaging.Interfaces; +using System; +using Reloaded.Messaging.Interfaces.Utilities; +using Reloaded.Messaging.Serializer.MessagePack; + +namespace Reloaded.Messaging.Tests.Messages; + +public struct Vector3MessagePackNoCompression : IMessage, NullCompressor>, IEquatable +{ + public sbyte GetMessageType() => (sbyte)MessageType.Vector3MessagePackNoCompression; + public MessagePackSerializer GetSerializer() => new(); + public NullCompressor? GetCompressor() => default; + + public float X { get; set; } + public float Y { get; set; } + public float Z { get; set; } + + public Vector3MessagePackNoCompression(float x, float y, float z) + { + X = x; + Y = y; + Z = z; + } + + // Auto-implemented by R# + public bool Equals(Vector3MessagePackNoCompression other) + { + return X.Equals(other.X) && Y.Equals(other.Y) && Z.Equals(other.Z); + } + + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) + return false; + + if (obj.GetType() != this.GetType()) + return false; + + return Equals((Vector3MessagePackNoCompression)obj); + } + + public override int GetHashCode() + { + unchecked + { + var hashCode = X.GetHashCode(); + hashCode = (hashCode * 397) ^ Y.GetHashCode(); + hashCode = (hashCode * 397) ^ Z.GetHashCode(); + return hashCode; + } + } +} \ No newline at end of file diff --git a/Source/Reloaded.Messaging.Tests/Messages/Vector3ReloadedMemoryDummyCompression.cs b/Source/Reloaded.Messaging.Tests/Messages/Vector3ReloadedMemoryDummyCompression.cs new file mode 100644 index 0000000..71202c4 --- /dev/null +++ b/Source/Reloaded.Messaging.Tests/Messages/Vector3ReloadedMemoryDummyCompression.cs @@ -0,0 +1,52 @@ +using System; +using Reloaded.Messaging.Interfaces; +using Reloaded.Messaging.Interfaces.Utilities; +using Reloaded.Messaging.Serializer.ReloadedMemory; + +namespace Reloaded.Messaging.Tests.Messages; + +public struct Vector3ReloadedMemoryDummyCompression : IMessage, NullCompressor>, IEquatable +{ + public sbyte GetMessageType() => (sbyte)MessageType.Vector3ReloadedMemoryDummyCompression; + public UnmanagedReloadedMemorySerializer GetSerializer() => new(); + public NullCompressor? GetCompressor() => new(); + + public float X; + public float Y; + public float Z; + + public Vector3ReloadedMemoryDummyCompression(float x, float y, float z) + { + X = x; + Y = y; + Z = z; + } + + // Auto-implemented by R# + public bool Equals(Vector3ReloadedMemoryDummyCompression other) + { + return X.Equals(other.X) && Y.Equals(other.Y) && Z.Equals(other.Z); + } + + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) + return false; + + if (obj.GetType() != this.GetType()) + return false; + + return Equals((Vector3ReloadedMemoryDummyCompression)obj); + } + + public override int GetHashCode() + { + unchecked + { + var hashCode = X.GetHashCode(); + hashCode = (hashCode * 397) ^ Y.GetHashCode(); + hashCode = (hashCode * 397) ^ Z.GetHashCode(); + return hashCode; + } + } +} \ No newline at end of file diff --git a/Source/Reloaded.Messaging.Tests/Messages/Vector3SystemTextJson.cs b/Source/Reloaded.Messaging.Tests/Messages/Vector3SystemTextJson.cs new file mode 100644 index 0000000..5e5969a --- /dev/null +++ b/Source/Reloaded.Messaging.Tests/Messages/Vector3SystemTextJson.cs @@ -0,0 +1,52 @@ +using Reloaded.Messaging.Interfaces; +using System; +using Reloaded.Messaging.Interfaces.Utilities; +using Reloaded.Messaging.Extras.Runtime; + +namespace Reloaded.Messaging.Tests.Messages; + +public struct Vector3SystemTextJson : IMessage, NullCompressor>, IEquatable +{ + public sbyte GetMessageType() => (sbyte)MessageType.Vector3SystemTextJson; + public SystemTextJsonSerializer GetSerializer() => new(); + public NullCompressor? GetCompressor() => default; + + public float X { get; set; } + public float Y { get; set; } + public float Z { get; set; } + + public Vector3SystemTextJson(float x, float y, float z) + { + X = x; + Y = y; + Z = z; + } + + // Auto-implemented by R# + public bool Equals(Vector3SystemTextJson other) + { + return X.Equals(other.X) && Y.Equals(other.Y) && Z.Equals(other.Z); + } + + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) + return false; + + if (obj.GetType() != this.GetType()) + return false; + + return Equals((Vector3SystemTextJson)obj); + } + + public override int GetHashCode() + { + unchecked + { + var hashCode = X.GetHashCode(); + hashCode = (hashCode * 397) ^ Y.GetHashCode(); + hashCode = (hashCode * 397) ^ Z.GetHashCode(); + return hashCode; + } + } +} \ No newline at end of file diff --git a/Source/Reloaded.Messaging.Tests/Messages/Vector3SystemTextJsonSourceGenerated.cs b/Source/Reloaded.Messaging.Tests/Messages/Vector3SystemTextJsonSourceGenerated.cs new file mode 100644 index 0000000..bf504f7 --- /dev/null +++ b/Source/Reloaded.Messaging.Tests/Messages/Vector3SystemTextJsonSourceGenerated.cs @@ -0,0 +1,61 @@ +namespace Reloaded.Messaging.Tests.Messages; + +#if NET6_0_OR_GREATER +using Reloaded.Messaging.Interfaces; +using System; +using System.Text.Json; +using Reloaded.Messaging.Interfaces.Utilities; +using System.Text.Json.Serialization; +using Reloaded.Messaging.Extras.Runtime; +[JsonSourceGenerationOptionsAttribute(GenerationMode = JsonSourceGenerationMode.Default)] +[JsonSerializable(typeof(Vector3SystemTextJsonSourceGenerated))] +internal partial class Vector3JsonContext : JsonSerializerContext +{ +} + +public struct Vector3SystemTextJsonSourceGenerated : IMessage, NullCompressor>, IEquatable +{ + public sbyte GetMessageType() => (sbyte)MessageType.Vector3SystemTextJson; + public SourceGeneratedSystemTextJsonSerializer GetSerializer() => new(Vector3JsonContext.Default.Vector3SystemTextJsonSourceGenerated); + public NullCompressor? GetCompressor() => default; + + public float X { get; set; } + public float Y { get; set; } + public float Z { get; set; } + + public Vector3SystemTextJsonSourceGenerated(float x, float y, float z) + { + X = x; + Y = y; + Z = z; + } + + // Auto-implemented by R# + public bool Equals(Vector3SystemTextJsonSourceGenerated other) + { + return X.Equals(other.X) && Y.Equals(other.Y) && Z.Equals(other.Z); + } + + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) + return false; + + if (obj.GetType() != this.GetType()) + return false; + + return Equals((Vector3SystemTextJsonSourceGenerated)obj); + } + + public override int GetHashCode() + { + unchecked + { + var hashCode = X.GetHashCode(); + hashCode = (hashCode * 397) ^ Y.GetHashCode(); + hashCode = (hashCode * 397) ^ Z.GetHashCode(); + return hashCode; + } + } +} +#endif \ No newline at end of file diff --git a/Source/Reloaded.Messaging.Tests/Messages/Vector3ZStandard.cs b/Source/Reloaded.Messaging.Tests/Messages/Vector3ZStandard.cs new file mode 100644 index 0000000..b17131e --- /dev/null +++ b/Source/Reloaded.Messaging.Tests/Messages/Vector3ZStandard.cs @@ -0,0 +1,52 @@ +using Reloaded.Messaging.Interfaces; +using System; +using Reloaded.Messaging.Compressor.ZStandard; +using Reloaded.Messaging.Serializer.ReloadedMemory; + +namespace Reloaded.Messaging.Tests.Messages; + +public struct Vector3ZStandard : IMessage, ZStandardCompressor>, IEquatable +{ + public sbyte GetMessageType() => (sbyte)MessageType.Vector3ZStandard; + public UnmanagedReloadedMemorySerializer GetSerializer() => new(); + public ZStandardCompressor GetCompressor() => new(); + + public float X { get; set; } + public float Y { get; set; } + public float Z { get; set; } + + public Vector3ZStandard(float x, float y, float z) + { + X = x; + Y = y; + Z = z; + } + + // Auto-implemented by R# + public bool Equals(Vector3ZStandard other) + { + return X.Equals(other.X) && Y.Equals(other.Y) && Z.Equals(other.Z); + } + + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) + return false; + + if (obj.GetType() != this.GetType()) + return false; + + return Equals((Vector3ZStandard)obj); + } + + public override int GetHashCode() + { + unchecked + { + var hashCode = X.GetHashCode(); + hashCode = (hashCode * 397) ^ Y.GetHashCode(); + hashCode = (hashCode * 397) ^ Z.GetHashCode(); + return hashCode; + } + } +} \ No newline at end of file diff --git a/Source/Reloaded.Messaging.Tests/Reloaded.Messaging.Tests.csproj b/Source/Reloaded.Messaging.Tests/Reloaded.Messaging.Tests.csproj index 0ede4a0..ee0eae0 100644 --- a/Source/Reloaded.Messaging.Tests/Reloaded.Messaging.Tests.csproj +++ b/Source/Reloaded.Messaging.Tests/Reloaded.Messaging.Tests.csproj @@ -1,9 +1,11 @@  - netcoreapp3.1;NET472;net5.0 + netcoreapp3.1;net6.0 preview false + true + enable @@ -27,10 +29,10 @@ + + - - diff --git a/Source/Reloaded.Messaging.Tests/Struct/StringMessage.cs b/Source/Reloaded.Messaging.Tests/Struct/StringMessage.cs deleted file mode 100644 index 27d8032..0000000 --- a/Source/Reloaded.Messaging.Tests/Struct/StringMessage.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Runtime.InteropServices; -using Reloaded.Messaging.Interfaces; -using Reloaded.Messaging.Messages; -using Reloaded.Messaging.Serializer.MessagePack; - -namespace Reloaded.Messaging.Tests.Struct; - -public struct StringMessage : IMessage -{ - public MessageType GetMessageType() => MessageType.String; - public ISerializer GetSerializer() => new MsgPackSerializer(); - public ICompressor GetCompressor() => null; - - public string Text { get; set; } - - public StringMessage(string text) - { - Text = text; - } - - /* Auto Generated by R# */ - public bool Equals(StringMessage other) - { - return string.Equals(Text, other.Text); - } - - public override bool Equals(object obj) - { - return obj is StringMessage other && Equals(other); - } - - public override int GetHashCode() - { - return (Text != null ? Text.GetHashCode() : 0); - } -} \ No newline at end of file diff --git a/Source/Reloaded.Messaging.Tests/Tests/Compression/CompressionTest.cs b/Source/Reloaded.Messaging.Tests/Tests/Compression/CompressionTest.cs deleted file mode 100644 index 8542aa6..0000000 --- a/Source/Reloaded.Messaging.Tests/Tests/Compression/CompressionTest.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System; -using Reloaded.Messaging.Compressor.ZStandard; -using Reloaded.Messaging.Messages; -using Reloaded.Messaging.Serializer.ReloadedMemory; -using Reloaded.Messaging.Tests.Struct; -using Xunit; -using ZstdNet; - -namespace Reloaded.Messaging.Tests.Tests.Compression; - -public class CompressionTest : IDisposable -{ - public void Dispose() - { - Overrides.SerializerOverride.Remove(typeof(Vector3)); - Overrides.CompressorOverride.Remove(typeof(Vector3)); - } - - [Fact] - public void CheckCompression() - { - var originalVector = new Vector3(235F, 10F, 5F); - var vectorMessage = new Message(originalVector); - - // Set serialization: Reloaded. - // Set compression: None - GC.Collect(); - Overrides.SerializerOverride[typeof(Vector3)] = new ReloadedMemorySerializer(false); - Overrides.CompressorOverride.Remove(typeof(Vector3)); - - var serializedUncompressed = vectorMessage.Serialize(); - var deserializedUncompressed = Message.Deserialize(serializedUncompressed); - Assert.Equal(originalVector, deserializedUncompressed); - - // Set compression: ZStandard - Overrides.CompressorOverride[typeof(Vector3)] = new ZStandardCompressor(new CompressionOptions(22)); - var serializedZStandard = vectorMessage.Serialize(); - var deserializedZStandard = Message.Deserialize(serializedZStandard); - - Assert.Equal(originalVector, deserializedZStandard); - Assert.NotEqual(serializedUncompressed, serializedZStandard); - - // Note: Compression here acts negatively. - } -} \ No newline at end of file diff --git a/Source/Reloaded.Messaging.Tests/Tests/Serialization/PureSerialize.cs b/Source/Reloaded.Messaging.Tests/Tests/Serialization/PureSerialize.cs deleted file mode 100644 index 23c2bca..0000000 --- a/Source/Reloaded.Messaging.Tests/Tests/Serialization/PureSerialize.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; -using Reloaded.Messaging.Interfaces; -using Reloaded.Messaging.Tests.Struct; -using Xunit; - -namespace Reloaded.Messaging.Tests.Tests.Serialization; - -public class PureSerialize -{ - - /* Tests Pure Serialization: No Message Passing involved. */ - - [Fact] - public void SerializeAndDeserialize() - { - var vector = new Vector3(0, 25, 100); - byte[] data = vector.Serialize(); - var newVector = Serializable.Deserialize(data); - Assert.Equal(vector, newVector); - } -} \ No newline at end of file diff --git a/Source/Reloaded.Messaging.Tests/Tests/Serialization/StringPassTest.cs b/Source/Reloaded.Messaging.Tests/Tests/Serialization/StringPassTest.cs deleted file mode 100644 index f30512c..0000000 --- a/Source/Reloaded.Messaging.Tests/Tests/Serialization/StringPassTest.cs +++ /dev/null @@ -1,91 +0,0 @@ -using System; -using System.Text.Json; -using System.Threading; -using LiteNetLib; -using Newtonsoft.Json; -using Reloaded.Messaging.Interfaces; -using Reloaded.Messaging.Messages; -using Reloaded.Messaging.Serializer.MessagePack; -using Reloaded.Messaging.Serializer.NewtonsoftJson; -using Reloaded.Messaging.Serializer.ReloadedMemory; -using Reloaded.Messaging.Serializer.SystemTextJson; -using Reloaded.Messaging.Structs; -using Reloaded.Messaging.Tests.Init; -using Reloaded.Messaging.Tests.Struct; -using Xunit; - -namespace Reloaded.Messaging.Tests.Tests.Serialization; - -public class StringPassTest : IDisposable -{ - private const string Message = "Test Message"; - private TestingHosts _hosts; - - public StringPassTest() - { - _hosts = new TestingHosts(); - } - - public void Dispose() - { - Overrides.SerializerOverride.Remove(typeof(StringMessage)); - Overrides.CompressorOverride.Remove(typeof(StringMessage)); - _hosts.Dispose(); - } - - [Fact(Timeout = 1000)] - public void MsgPackPassString() - { - Overrides.SerializerOverride[typeof(StringMessage)] = new MsgPackSerializer(); - Overrides.CompressorOverride[typeof(StringMessage)] = null; - PassString(); - } - - [Fact(Timeout = 1000)] - public void ReloadedPassString() - { - Overrides.SerializerOverride[typeof(StringMessage)] = new ReloadedMemorySerializer(true); - Overrides.CompressorOverride[typeof(StringMessage)] = null; - PassString(); - } - - [Fact(Timeout = 1000)] - public void SystemTextJsonPassString() - { - Overrides.SerializerOverride[typeof(StringMessage)] = new SystemTextJsonSerializer(new JsonSerializerOptions()); - Overrides.CompressorOverride[typeof(StringMessage)] = null; - PassString(); - } - - [Fact(Timeout = 1000)] - public void NewtonsoftPassString() - { - Overrides.SerializerOverride[typeof(StringMessage)] = new NewtonsoftJsonSerializer(new JsonSerializerSettings()); - Overrides.CompressorOverride[typeof(StringMessage)] = null; - PassString(); - } - - private void PassString() - { - string delivered = default; - - // Message handling method - void Handler(ref NetMessage netMessage) - { - delivered = netMessage.Message.Text; - } - - // Setup client. - _hosts.SimpleClient.MessageHandler.AddOrOverrideHandler(Handler); - - // Send Message. - var stringMessage = new Message(new StringMessage(Message)); - var data = stringMessage.Serialize(); - _hosts.SimpleServer.NetManager.FirstPeer.Send(data, DeliveryMethod.ReliableOrdered); - - while (delivered == default) - Thread.Sleep(16); - - Assert.Equal(Message, delivered); - } -} \ No newline at end of file diff --git a/Source/Reloaded.Messaging.Tests/Tests/Serialization/VectorPassTest.cs b/Source/Reloaded.Messaging.Tests/Tests/Serialization/VectorPassTest.cs deleted file mode 100644 index a3262c0..0000000 --- a/Source/Reloaded.Messaging.Tests/Tests/Serialization/VectorPassTest.cs +++ /dev/null @@ -1,90 +0,0 @@ -using System; -using System.Text.Json; -using System.Threading; -using LiteNetLib; -using Newtonsoft.Json; -using Reloaded.Messaging.Messages; -using Reloaded.Messaging.Serializer.MessagePack; -using Reloaded.Messaging.Serializer.NewtonsoftJson; -using Reloaded.Messaging.Serializer.ReloadedMemory; -using Reloaded.Messaging.Serializer.SystemTextJson; -using Reloaded.Messaging.Structs; -using Reloaded.Messaging.Tests.Init; -using Xunit; -using Vector3 = Reloaded.Messaging.Tests.Struct.Vector3; - -namespace Reloaded.Messaging.Tests.Tests.Serialization; - -public class VectorPassTest : IDisposable -{ - private TestingHosts _hosts; - - public VectorPassTest() - { - _hosts = new TestingHosts(); - } - - public void Dispose() - { - Overrides.SerializerOverride.Remove(typeof(Vector3)); - Overrides.CompressorOverride.Remove(typeof(Vector3)); - _hosts.Dispose(); - } - - [Fact(Timeout = 1000)] - public void MsgPackPassVector3() - { - Overrides.SerializerOverride[typeof(Vector3)] = new MsgPackSerializer(); - Overrides.CompressorOverride.Remove(typeof(Vector3)); - PassVector3(); - } - - [Fact(Timeout = 1000)] - public void ReloadedPassVector3() - { - Overrides.SerializerOverride[typeof(Vector3)] = new ReloadedMemorySerializer(false); - Overrides.CompressorOverride.Remove(typeof(Vector3)); - PassVector3(); - } - - [Fact(Timeout = 1000)] - public void SystemTextJsonPassVector3() - { - Overrides.SerializerOverride[typeof(Vector3)] = new SystemTextJsonSerializer(new JsonSerializerOptions()); - Overrides.CompressorOverride.Remove(typeof(Vector3)); - PassVector3(); - } - - [Fact(Timeout = 1000)] - public void NewtonsoftPassVector3() - { - Overrides.SerializerOverride[typeof(Vector3)] = new NewtonsoftJsonSerializer(new JsonSerializerSettings()); - Overrides.CompressorOverride.Remove(typeof(Vector3)); - PassVector3(); - } - - private void PassVector3() - { - Vector3 message = new Vector3(1.0F, 235.0F, 100.0F); - Vector3 delivered = default; - - // Message handling method - void Handler(ref NetMessage netMessage) - { - delivered = netMessage.Message; - } - - // Setup client - _hosts.SimpleClient.MessageHandler.AddOrOverrideHandler(Handler); - - // Send Message. - var vectorMessage = new Message(message); - var data = vectorMessage.Serialize(); - _hosts.SimpleServer.NetManager.FirstPeer.Send(data, DeliveryMethod.ReliableOrdered); - - while (delivered.Equals(default(Vector3))) - Thread.Sleep(16); - - Assert.Equal(message, delivered); - } -} \ No newline at end of file diff --git a/Source/Reloaded.Messaging.sln b/Source/Reloaded.Messaging.sln index 745024d..0508c65 100644 --- a/Source/Reloaded.Messaging.sln +++ b/Source/Reloaded.Messaging.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.29006.145 +# Visual Studio Version 17 +VisualStudioVersion = 17.3.32611.2 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Reloaded.Messaging", "Reloaded.Messaging\Reloaded.Messaging.csproj", "{0F5EE206-32FC-404F-80FB-E02B37AE2855}" EndProject @@ -15,9 +15,13 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Reloaded.Messaging.Compress EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Reloaded.Messaging.Interfaces", "Reloaded.Messaging.Interfaces\Reloaded.Messaging.Interfaces.csproj", "{90FF55E7-AFAC-4089-BAAD-B9BFF51E2447}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Reloaded.Messaging.Serializer.SystemTextJson", "Reloaded.Messaging.Serializer.SystemTextJson\Reloaded.Messaging.Serializer.SystemTextJson.csproj", "{E80F0107-A3A7-4DDF-BA5F-3C771F54062E}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Reloaded.Messaging.Extras.Runtime", "Reloaded.Messaging.Extras.Runtime\Reloaded.Messaging.Extras.Runtime.csproj", "{E80F0107-A3A7-4DDF-BA5F-3C771F54062E}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Reloaded.Messaging.Serializer.NewtonsoftJson", "Reloaded.Messaging.Serializer.NewtonsoftJson\Reloaded.Messaging.Serializer.NewtonsoftJson.csproj", "{62D6AE60-B5CB-4E13-9A98-BD8DCB0CBBE6}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Reloaded.Messaging.Host.LiteNetLib", "Reloaded.Messaging.Host.LiteNetLib\Reloaded.Messaging.Host.LiteNetLib.csproj", "{1189FFF2-616A-478D-99C7-C1FDC068413C}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Extra", "Extra", "{73B9A805-15CC-4DE4-8039-7FEBB366D4E1}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Reloaded.Messaging.Benchmarks", "Reloaded.Messaging.Benchmarks\Reloaded.Messaging.Benchmarks.csproj", "{05BAA584-FEFF-4180-8035-6F075A17A051}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -53,14 +57,21 @@ Global {E80F0107-A3A7-4DDF-BA5F-3C771F54062E}.Debug|Any CPU.Build.0 = Debug|Any CPU {E80F0107-A3A7-4DDF-BA5F-3C771F54062E}.Release|Any CPU.ActiveCfg = Release|Any CPU {E80F0107-A3A7-4DDF-BA5F-3C771F54062E}.Release|Any CPU.Build.0 = Release|Any CPU - {62D6AE60-B5CB-4E13-9A98-BD8DCB0CBBE6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {62D6AE60-B5CB-4E13-9A98-BD8DCB0CBBE6}.Debug|Any CPU.Build.0 = Debug|Any CPU - {62D6AE60-B5CB-4E13-9A98-BD8DCB0CBBE6}.Release|Any CPU.ActiveCfg = Release|Any CPU - {62D6AE60-B5CB-4E13-9A98-BD8DCB0CBBE6}.Release|Any CPU.Build.0 = Release|Any CPU + {1189FFF2-616A-478D-99C7-C1FDC068413C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1189FFF2-616A-478D-99C7-C1FDC068413C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1189FFF2-616A-478D-99C7-C1FDC068413C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1189FFF2-616A-478D-99C7-C1FDC068413C}.Release|Any CPU.Build.0 = Release|Any CPU + {05BAA584-FEFF-4180-8035-6F075A17A051}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {05BAA584-FEFF-4180-8035-6F075A17A051}.Debug|Any CPU.Build.0 = Debug|Any CPU + {05BAA584-FEFF-4180-8035-6F075A17A051}.Release|Any CPU.ActiveCfg = Release|Any CPU + {05BAA584-FEFF-4180-8035-6F075A17A051}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {05BAA584-FEFF-4180-8035-6F075A17A051} = {73B9A805-15CC-4DE4-8039-7FEBB366D4E1} + EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {E6F63326-4EEA-4D4E-BB44-60FD2065CB3E} EndGlobalSection diff --git a/Source/Reloaded.Messaging/MessageDispatcher.cs b/Source/Reloaded.Messaging/MessageDispatcher.cs new file mode 100644 index 0000000..f3431df --- /dev/null +++ b/Source/Reloaded.Messaging/MessageDispatcher.cs @@ -0,0 +1,67 @@ +using System; +using Reloaded.Messaging.Interfaces; +using Reloaded.Messaging.Messages; +using Reloaded.Messaging.Utilities; + +#if NET5_0_OR_GREATER +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +#endif + +namespace Reloaded.Messaging; + +/// +/// Provides a generic mechanism for dispatching messages to registered handlers. +/// +/// Type of parameter. +public struct MessageDispatcher : IMessageDispatcher +{ + // Note: We allocate 255 handlers to remove a branch in `GetHandlerForType` in cases where bad actor may pass invalid value. + // bit wasteful memory wise but it is what it is. + + private IMessageHandlerBase?[] _handlers; + + /// + public MessageDispatcher() => _handlers = new IMessageHandlerBase[byte.MaxValue]; + + /// + /// Gets a handler that handles a specific message type. + /// + /// The type of message requested. + /// Handler for the specific message. Might be null. + public ref IMessageHandlerBase? GetHandlerForType(byte messageType) + { +#if NET5_0_OR_GREATER + ref var writer = ref MemoryMarshal.GetArrayDataReference(_handlers); + return ref Unsafe.Add(ref writer, (int)messageType); +#else + return ref _handlers[messageType]; +#endif + } + + /// + /// Sets a handler for a specific message type. + /// + public void AddOrOverrideHandler(IMessageHandlerBase handler) => _handlers[handler.GetMessageType()] = handler; + + /// + /// Removes a handler assigned to a specific message type. + /// + public void RemoveHandler(byte messageType) => _handlers[messageType] = null; + + /// + /// Given a raw network message, decodes the message and delegates it to an appropriate handling method. + /// + /// Data containing a packed Reloaded.Messaging message. + /// The extra data associated with this request. +#if NET5_0_OR_GREATER + [SkipLocalsInit] +#endif + public void Dispatch(Span data, ref TExtraData extraData) + { + HeaderReader.ReadHeader(data, out var messageType, out var sizeAfterCompression, out var headerSize); + ref var handler = ref GetHandlerForType((byte)messageType); + handler?.HandleMessage(SpanExtensions.SliceFast(data, headerSize), sizeAfterCompression, ref extraData); + } +} + diff --git a/Source/Reloaded.Messaging/MessageHandler.cs b/Source/Reloaded.Messaging/MessageHandler.cs deleted file mode 100644 index 30ae7c5..0000000 --- a/Source/Reloaded.Messaging/MessageHandler.cs +++ /dev/null @@ -1,68 +0,0 @@ -using System.Collections.Generic; -using Reloaded.Messaging.Interfaces; -using Reloaded.Messaging.Messages; -using Reloaded.Messaging.Structs; - -namespace Reloaded.Messaging; - -/// -/// Provides a generic mechanism for dispatching messages received from a client or server. -/// Works by assigning functions to specified message "types", declared by TMessageType. -/// -/// Type of value to map to individual message handlers. -public class MessageHandler where TMessageType : unmanaged -{ - private Dictionary _mapping; - - /// - public MessageHandler() - { - _mapping = new Dictionary(); - } - - /// - /// Given a raw network message, decodes the message and delegates it to an appropriate handling method. - /// - public void Handle(ref RawNetMessage parameters) - { - var messageType = MessageBase.GetMessageType(parameters.Message); - if (_mapping.TryGetValue(messageType, out RawNetMessageHandler value)) - value(ref parameters); - } - - /// - /// Sets a method to execute handling a specific - /// - public void AddOrOverrideHandler(Handler handler) where TStruct : IMessage, new() - { - var messageType = MessageExtensions.GetMessageType(new TStruct()); - AddOrOverrideHandler(messageType, handler); - } - - /// - /// Sets a method to execute handling a specific - /// - public void AddOrOverrideHandler(TMessageType messageType, Handler handler) where TStruct : IMessage, new() - { - RawNetMessageHandler parameters = delegate (ref RawNetMessage rawMessage) - { - var message = Message.Deserialize(rawMessage.Message); - var netMessage = new NetMessage(ref message, ref rawMessage); - handler(ref netMessage); - }; - - _mapping[messageType] = parameters; - } - - /// - /// Removes the current method assigned to a handle a message of a specific - /// - public void RemoveHandler(TMessageType messageType) - { - _mapping.Remove(messageType); - } - - /// - public delegate void Handler(ref NetMessage netMessage); - private delegate void RawNetMessageHandler(ref RawNetMessage rawNetMessage); -} \ No newline at end of file diff --git a/Source/Reloaded.Messaging/Messages/Disposables/ReusableSingletonMemoryStream.cs b/Source/Reloaded.Messaging/Messages/Disposables/ReusableSingletonMemoryStream.cs new file mode 100644 index 0000000..de94f71 --- /dev/null +++ b/Source/Reloaded.Messaging/Messages/Disposables/ReusableSingletonMemoryStream.cs @@ -0,0 +1,27 @@ +using System; +using Microsoft.IO; +using Reloaded.Messaging.Utilities; + +namespace Reloaded.Messaging.Messages.Disposables; + +/// +/// Wrapper of a recyclable memory stream exposing the raw data underneath. +/// +public struct ReusableSingletonMemoryStream : IDisposable +{ + internal RecyclableMemoryStream Stream; + + /// + /// Provides access to the underlying span. + /// + public Span Span => SpanExtensions.AsSpanFast(Stream.GetBuffer(), (int)Stream.Length); + + /// + /// The stream in question. + public ReusableSingletonMemoryStream(RecyclableMemoryStream stream) { Stream = stream; } + + /// + /// Disposes the underlying stream. + /// + public void Dispose() => Stream.SetLength(0); +} \ No newline at end of file diff --git a/Source/Reloaded.Messaging/Messages/HeaderReader.cs b/Source/Reloaded.Messaging/Messages/HeaderReader.cs new file mode 100644 index 0000000..f1db778 --- /dev/null +++ b/Source/Reloaded.Messaging/Messages/HeaderReader.cs @@ -0,0 +1,59 @@ +using System; +using System.Buffers.Binary; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace Reloaded.Messaging.Messages; + +/// +/// Reads the header of a packed message. +/// +public struct HeaderReader +{ + /// + /// Size of standard header, without compression. + /// + public const int StandardHeaderSize = 1; + + /// + /// Size of standard header, with compression. + /// + public const int CompressedHeaderSize = 5; + + /// + public const byte CompressionFlag = 0b10000000; + + + /// + public const int HeaderDecompressedDataSize = sizeof(uint); + + /// + /// Reads the message header of a packaged message. + /// + /// Span containing the header at the start. + /// The type of message in this header. + /// The size of the compressed payload. This is -1 if there is no compression. + /// Size of the header at the start of the span. +#if NET5_0_OR_GREATER + [SkipLocalsInit] +#endif + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ReadHeader(Span data, out sbyte messageType, out int sizeAfterDecompression, out int headerSize) + { + messageType = (sbyte)MemoryMarshal.GetReference(data); + + if ((messageType & CompressionFlag) == CompressionFlag) + { + sizeAfterDecompression = Unsafe.AsRef(Unsafe.Add(ref MemoryMarshal.GetReference(data), 1)); + messageType = (sbyte)(messageType ^ CompressionFlag); + if (!BitConverter.IsLittleEndian) // Evaluated at JIT time. + sizeAfterDecompression = BinaryPrimitives.ReverseEndianness(sizeAfterDecompression); + + headerSize = CompressedHeaderSize; + return; + } + + headerSize = StandardHeaderSize; + sizeAfterDecompression = -1; + } +} \ No newline at end of file diff --git a/Source/Reloaded.Messaging/Messages/IMessageExtensions.cs b/Source/Reloaded.Messaging/Messages/IMessageExtensions.cs deleted file mode 100644 index c6b82bc..0000000 --- a/Source/Reloaded.Messaging/Messages/IMessageExtensions.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Reloaded.Messaging.Interfaces; - -namespace Reloaded.Messaging.Messages; - -/// -/// Hosts various extension methods related to the interface. -/// -public static class MessageExtensions -{ - /// - /// Retrieves the message type (key) for a given type. - /// - /// The message to get type for. - /// Used to instantiate and get message type (key) as single liner in source code. - public static TMessageType GetMessageType(this IMessage message) where TMessageType : unmanaged - { - return message.GetMessageType(); - } -} \ No newline at end of file diff --git a/Source/Reloaded.Messaging/Messages/Message.cs b/Source/Reloaded.Messaging/Messages/Message.cs deleted file mode 100644 index 18d85c0..0000000 --- a/Source/Reloaded.Messaging/Messages/Message.cs +++ /dev/null @@ -1,121 +0,0 @@ -using System; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using Reloaded.Messaging.Interfaces; - -namespace Reloaded.Messaging.Messages; - -/// -/// -/// -/// The key for the message. -/// -public unsafe class Message : MessageBase where TStruct : IMessage, new() where TMessageType : unmanaged -{ - // ReSharper disable StaticMemberInGenericType - private static ISerializer DefaultSerializer { get; set; } - private static ICompressor DefaultCompressor { get; set; } - private static bool DefaultCompressorSet { get; set; } - - // ReSharper restore StaticMemberInGenericType - - /// - /// The actual message backed by this class. - /// - public TStruct ActualMessage { get; set; } - - /// - /// - /// - /// - public Message(TStruct message) - { - ActualMessage = message; - } - - /// - /// Serializes the current instance and returns an array of bytes representing the instance. - /// - public byte[] Serialize() - { - // Perform serialization. - var serializer = GetSerializer(); - var message = ActualMessage; - var encodedMessage = serializer.Serialize(ref message); - - // Allocate memory for result and write header. - var result = new byte[encodedMessage.Length + sizeof(TMessageType)]; - var resultSpan = result.AsSpan(); - var messageType = ActualMessage.GetMessageType(); - -#if (USE_NATIVE_SPAN_API) - var readOnlyMessageType = MemoryMarshal.CreateReadOnlySpan(ref messageType, sizeof(TMessageType)); - var readOnlyMessageTypeBytes = MemoryMarshal.AsBytes(readOnlyMessageType); - readOnlyMessageTypeBytes.CopyTo(resultSpan); -#else - byte* bytes = (byte*)Unsafe.AsPointer(ref messageType); - var readOnlyMessageTypeBytes = new Span(bytes, sizeof(TMessageType)); - readOnlyMessageTypeBytes.CopyTo(resultSpan); -#endif - - // Append serialized data. - resultSpan = resultSpan.Slice(sizeof(TMessageType)); - encodedMessage.AsSpan().CopyTo(resultSpan); - - var compressor = GetCompressor(); - if (compressor != null) - result = compressor.Compress(result); - - return result; - } - - /// - /// Deserializes a given set of bytes into a usable struct. - /// - public static TStruct Deserialize(byte[] serializedBytes) - { - // Get decompressor. - var compressor = GetCompressor(); - if (compressor != null) - serializedBytes = compressor.Decompress(serializedBytes); - - // Get serializer - var serializer = GetSerializer(); - - // Read message. - var messageSegment = serializedBytes.AsSpan(sizeof(TMessageType)).ToArray(); - var message = serializer.Deserialize(messageSegment); - return message; - - // Note: No need to read MessageType. MessageType was only necessary to link a message to correct handler. - } - - private static ISerializer GetSerializer() - { - if (Overrides.SerializerOverride.TryGetValue(typeof(TStruct), out ISerializer value)) - return value; - - if (DefaultSerializer == null) - { - var defaultStruct = new TStruct(); - DefaultSerializer = ((IMessage)defaultStruct).GetSerializer(); - } - - return DefaultSerializer; - } - - private static ICompressor GetCompressor() - { - if (Overrides.CompressorOverride.TryGetValue(typeof(TStruct), out ICompressor value)) - return value; - - if (! DefaultCompressorSet) - { - var defaultStruct = new TStruct(); - DefaultCompressor = ((IMessage)defaultStruct).GetCompressor(); - DefaultCompressorSet = true; - } - - return DefaultCompressor; - } -} \ No newline at end of file diff --git a/Source/Reloaded.Messaging/Messages/MessageBase.cs b/Source/Reloaded.Messaging/Messages/MessageBase.cs deleted file mode 100644 index 1e324b7..0000000 --- a/Source/Reloaded.Messaging/Messages/MessageBase.cs +++ /dev/null @@ -1,21 +0,0 @@ -namespace Reloaded.Messaging.Messages; - -/// -/// The base class for the class, used for dealing with message type specific things. -/// -/// -public unsafe class MessageBase where TMessageType : unmanaged -{ - /// - /// Retrieves the message type by raw array of values. - /// - /// The raw serialized bytes containing the message. - /// The type of the message. - public static TMessageType GetMessageType(byte[] serializedBytes) - { - fixed (byte* arrayPtr = serializedBytes) - { - return *(TMessageType*) arrayPtr; - } - } -} \ No newline at end of file diff --git a/Source/Reloaded.Messaging/Messages/MessageReader.cs b/Source/Reloaded.Messaging/Messages/MessageReader.cs new file mode 100644 index 0000000..960ba5b --- /dev/null +++ b/Source/Reloaded.Messaging/Messages/MessageReader.cs @@ -0,0 +1,51 @@ +using Reloaded.Messaging.Interfaces; +using System; +using Reloaded.Messaging.Utilities; + +namespace Reloaded.Messaging.Messages; + +/// +/// Struct used for deserializing of messages. +/// +/// The structure represented by the message. +/// Type of serializer used. +/// Type of compressor used. +public unsafe struct MessageReader where TStruct : IMessage + where TSerializer : ISerializer + where TCompressor : ICompressor +{ + private TCompressor? _compressor; + private TSerializer _serializer; + + /// + /// Deserializes messages with specified type. + /// + /// A sample structure to extract compressor and serializer from. + public MessageReader(in TStruct sample) + { + _compressor = sample.GetCompressor(); + _serializer = sample.GetSerializer(); + } + + /// + /// Deserializes a given set of bytes into a usable struct. + /// + /// The raw bytes containing the message, without message header. + /// Expected size after decompression, ignored if no compression will be used. + public TStruct Deserialize(Span serializedBytes, int decompressedSize) + { + // Get decompressor. + if (_compressor != null) + { + // Decompress + using var decompressedRental = new ArrayRental(decompressedSize); + var decompressedSpan = decompressedRental.Span; + _compressor.Decompress(serializedBytes, decompressedSpan); + + // Deserialize. + return _serializer.Deserialize(decompressedSpan); + } + + return _serializer.Deserialize(serializedBytes); + } +} \ No newline at end of file diff --git a/Source/Reloaded.Messaging/Messages/MessageWriter.cs b/Source/Reloaded.Messaging/Messages/MessageWriter.cs new file mode 100644 index 0000000..12d64ff --- /dev/null +++ b/Source/Reloaded.Messaging/Messages/MessageWriter.cs @@ -0,0 +1,121 @@ +using System; +using System.Buffers.Binary; +using System.IO; +using System.Runtime.CompilerServices; +using Microsoft.IO; +using Reloaded.Messaging.Interfaces; +using Reloaded.Messaging.Interfaces.Message; +using Reloaded.Messaging.Messages.Disposables; +using Reloaded.Messaging.Utilities; + +namespace Reloaded.Messaging.Messages; + +/// +/// Writes single messages to be sent via the network. +/// +/// The structure represented by the message. +/// Structure used to perform serialization. +/// Structure used to perform compression. +public static unsafe class MessageWriter where TStruct : IMessage + where TSerializer : ISerializer + where TCompressor : ICompressor +{ + /// + /// Serializes the current instance and returns a disposable memory array of data representing the instance. + /// +#if NET5_0_OR_GREATER + [SkipLocalsInit] +#endif + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ReusableSingletonMemoryStream Serialize(ref TStruct data) + { + // Get the compressor. + // If there is no compressor, we can write data type directly and then ser + var compressor = data.GetCompressor(); + if (compressor != null) + return SerializeToRecyclableMemoryStream(ref data, Pool.MessageStreamPerThread(), compressor); + else + return SerializeToRecyclableMemoryStream(ref data, Pool.MessageStreamPerThread()); + } + + /// + /// Serializes the current structure to, a provided with compression.

+ /// Internal-ish API intended for benchmarking. + ///
+ /// The data to serialize. + /// The memory stream to serialize to. + /// The compressor to use. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ReusableSingletonMemoryStream SerializeToRecyclableMemoryStream(ref TStruct data, RecyclableMemoryStream messageStream, TCompressor compressor) + { + // Serialize message first. + var serializer = data.GetSerializer(); + using var serializeStream = new ReusableSingletonMemoryStream(Pool.CompressionStreamPerThread()); + serializer.Serialize(ref data, serializeStream.Stream); + var serializedSpan = serializeStream.Span; + var decompressedSize = serializedSpan.Length; + + // Write data type. + var messageTypePacked = data.GetMessageType() | HeaderReader.CompressionFlag; + messageStream.WriteByte((byte)messageTypePacked); + + // Reserve space for compressed data. + var compressedLengthPos = messageStream.Position; + messageStream.Seek(HeaderReader.HeaderDecompressedDataSize, SeekOrigin.Current); + var compressedDataStartPos = messageStream.Position; + + // Compress directly into stream. + var maxCompressedSize = compressor.GetMaxCompressedSize((int)serializeStream.Stream.Length); + var compressedSpan = messageStream.GetSpan(maxCompressedSize); + int numCompressed = compressor.Compress(serializedSpan, compressedSpan); + + // Write compressed size + messageStream.Seek(compressedLengthPos, SeekOrigin.Begin); + if (!BitConverter.IsLittleEndian) // evaluated at JIT time, it's an intrinsic. + decompressedSize = BinaryPrimitives.ReverseEndianness(decompressedSize); + + messageStream.Write(SpanExtensions.AsByteSpanFast(&decompressedSize)); + + // Set length (to account for compressed # bytes). + messageStream.SetLength(compressedDataStartPos + numCompressed); + return new ReusableSingletonMemoryStream(messageStream); + } + + /// + /// Serializes the current structure to, a provided without using compression.

+ /// Internal-ish API intended for benchmarking. + ///
+ /// The data to serialize. + /// The memory stream to serialize to. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ReusableSingletonMemoryStream SerializeToRecyclableMemoryStream(ref TStruct data, RecyclableMemoryStream messageStream) + { + // If there is no compression, we can write type then data directly. + messageStream.WriteByte((byte)data.GetMessageType()); + data.GetSerializer().Serialize(ref data, messageStream); + return new ReusableSingletonMemoryStream(messageStream); + } +} + +/// +/// Extension methods for easier serialization and deserialization. +/// +public static unsafe class MessageWriterExtensions +{ + // Note: It's a mess but has no impact on JIT-ted code. + + /// + /// Serializes the current instance and returns a disposable memory array of data representing the instance. + /// +#if NET5_0_OR_GREATER + [SkipLocalsInit] +#endif + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ReusableSingletonMemoryStream Serialize(this IMessage dummy0, ref TStruct data, TSerializer? dummy1 = default, TCompressor? dummy2 = default) + where TStruct : IMessage + where TSerializer : ISerializer + where TCompressor : ICompressor + { + return MessageWriter.Serialize(ref data); + } +} \ No newline at end of file diff --git a/Source/Reloaded.Messaging/Overrides.cs b/Source/Reloaded.Messaging/Overrides.cs deleted file mode 100644 index 979fe51..0000000 --- a/Source/Reloaded.Messaging/Overrides.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System; -using System.Collections.Generic; -using Reloaded.Messaging.Interfaces; - -namespace Reloaded.Messaging; - -/// -/// Class which provides, client-side the ability to override serializers and compressors for specified types. -/// Use either for testing or benchmarking. -/// -public static class Overrides -{ - /// - /// Allows you to override the serializer for a specific type. - /// - public static Dictionary SerializerOverride { get; } = new Dictionary(); - - /// - /// Allows you to override the compressor for a specific type. - /// - public static Dictionary CompressorOverride { get; } = new Dictionary(); -} \ No newline at end of file diff --git a/Source/Reloaded.Messaging/ReferenceMessageHandler.cs b/Source/Reloaded.Messaging/ReferenceMessageHandler.cs new file mode 100644 index 0000000..fefcaa2 --- /dev/null +++ b/Source/Reloaded.Messaging/ReferenceMessageHandler.cs @@ -0,0 +1,70 @@ +using System; +using System.Runtime.CompilerServices; +using Reloaded.Messaging.Interfaces; +using Reloaded.Messaging.Messages; + +namespace Reloaded.Messaging; + +/// +/// The base class for message handlers. +/// +public class ReferenceMessageHandler : IMessageHandlerBase + where TStruct : IMessage + where TCompressor : ICompressor + where TSerializer : ISerializer + where TMsgRefAction : IMsgRefAction +{ + private MessageReader _deserializer; + private TMsgRefAction _refAction = default!; + private sbyte _messageType; + + /// + /// No default constructor. + /// + private ReferenceMessageHandler() { } + + /// + /// Sample structure to extract message deserializer from. + /// + /// Sample structure + /// Action to perform for each received message. + public ReferenceMessageHandler(in TStruct sample, TMsgRefAction action) + { + _deserializer = new MessageReader(sample); + _messageType = sample.GetMessageType(); + _refAction = action; + } + + /// + public sbyte GetMessageType() => _messageType; + + /// + public void HandleMessage(Span data, int decompressedSize, ref TExtraData extraData) + { + var deserialized = _deserializer.Deserialize(data, decompressedSize); + _refAction.OnMessageReceive(ref deserialized, ref extraData); + } + + /// + /// Deserializes the message from a raw array of bytes. + /// + /// The data to deserialize. + /// Decompressed size of data. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public TStruct Deserialize(Span data, int decompressedSize) => _deserializer.Deserialize(data, decompressedSize); +} + +/// +/// Provides a function to execute for the . +/// +/// The structure of received message. +/// Extra data held by the message. +public interface IMsgRefAction +{ + /// + /// Runs the code associated with the reference action. + /// + /// + /// + void OnMessageReceive(ref TStruct received, ref TExtraData data); +} \ No newline at end of file diff --git a/Source/Reloaded.Messaging/Reloaded.Messaging.csproj b/Source/Reloaded.Messaging/Reloaded.Messaging.csproj index 1e8bcf5..bee2d7d 100644 --- a/Source/Reloaded.Messaging/Reloaded.Messaging.csproj +++ b/Source/Reloaded.Messaging/Reloaded.Messaging.csproj @@ -2,29 +2,28 @@ netstandard2.1;netstandard2.0;netcoreapp3.1;net5.0 - false preview Sewer56 - Reloaded (Mod Loader) II's extensible "event-like" solution for passing messages across a local or remote network that sits ontop of LiteNetLib. + Reloaded II's high performance solution for adding high performance, near zero-overhead message packing to existing libraries. LICENSE.md https://github.com/Reloaded-Project/Reloaded.Messaging - https://avatars1.githubusercontent.com/u/45473408 https://github.com/Reloaded-Project/Reloaded.Messaging true Sewer56 - 1.3.0 + 2.0.0 true - $(DefineConstants);USE_NATIVE_SPAN_API - $(DefineConstants);USE_NATIVE_UNSAFE + $(DefineConstants);USE_NATIVE_SPAN_API + $(DefineConstants);USE_NATIVE_UNSAFE 1701;1702;NU5104 true NuGet-Icon.png + enable - + diff --git a/Source/Reloaded.Messaging/SimpleHost.cs b/Source/Reloaded.Messaging/SimpleHost.cs deleted file mode 100644 index 204896c..0000000 --- a/Source/Reloaded.Messaging/SimpleHost.cs +++ /dev/null @@ -1,84 +0,0 @@ -using System; -using LiteNetLib; -using Reloaded.Messaging.Structs; - -namespace Reloaded.Messaging; - -/// -/// Provides a simple client or host based off of LiteNetLib. -/// -public class SimpleHost : IDisposable where TMessageType : unmanaged -{ - /// - /// The password necessary to join this host. If it does not match, incoming clients will be rejected. - /// - public string Password { get; set; } - - /// - /// Set to true to accept incoming clients, else reject all clients. - /// - public bool AcceptClients { get; set; } - - /// - /// Event for handling connection requests. - /// - public event EventBasedNetListener.OnConnectionRequest ConnectionRequestEvent; - - /// - /// Dispatcher for individual (s) to your events. - /// - public MessageHandler MessageHandler { get; private set; } - - /// - public EventBasedNetListener Listener { get; private set; } - - /// - public NetManager NetManager { get; private set; } - - /// - /// Provides a simple client or host based off of LiteNetLib. - /// - /// Set to true to accept incoming clients, else reject all requests. - /// The password necessary to join. - public SimpleHost(bool acceptClients, string password = "") - { - Password = password; - AcceptClients = acceptClients; - MessageHandler = new MessageHandler(); - Listener = new EventBasedNetListener(); - Listener.NetworkReceiveEvent += OnNetworkReceive; - Listener.ConnectionRequestEvent += ListenerOnConnectionRequestEvent; - - NetManager = new NetManager(Listener); - NetManager.UnsyncedEvents = true; - NetManager.AutoRecycle = true; - } - - /// - public void Dispose() - { - NetManager.Stop(); - } - - private void ListenerOnConnectionRequestEvent(ConnectionRequest request) - { - if (ConnectionRequestEvent != null) - { - ConnectionRequestEvent(request); - return; - } - - if (AcceptClients) - request.AcceptIfKey(Password); - else - request.Reject(); - } - - /* On each message received. */ - private void OnNetworkReceive(NetPeer peer, NetPacketReader reader, byte channel, DeliveryMethod deliverymethod) - { - byte[] rawBytes = reader.GetRemainingBytes(); - var rawNetMessage = new RawNetMessage(rawBytes, peer, reader, deliverymethod); - MessageHandler.Handle(ref rawNetMessage); - } -} \ No newline at end of file diff --git a/Source/Reloaded.Messaging/Structs/NetMessage.cs b/Source/Reloaded.Messaging/Structs/NetMessage.cs deleted file mode 100644 index 4163786..0000000 --- a/Source/Reloaded.Messaging/Structs/NetMessage.cs +++ /dev/null @@ -1,42 +0,0 @@ -using LiteNetLib; - -namespace Reloaded.Messaging.Structs; - -/// -/// Encapsulates a message with a specific structure received from the network as a raw array of bytes. -/// -public struct NetMessage -{ - /// - /// The message received from the peer. - /// - public TStruct Message { get; private set; } - - /// - /// The peer which has sent you the message. - /// - public NetPeer Peer { get; private set; } - - /// - /// Can be used to read the message, if desired. - /// - public NetPacketReader PacketReader { get; private set; } - - /// - /// The method via which the package was delivered. - /// - public DeliveryMethod DeliveryMethod { get; private set; } - - /// - /// Encapsulates a raw message received from the network. - /// - /// The message in question. - /// The raw message from which this message should be constructed with. - public NetMessage(ref TStruct message, ref RawNetMessage rawMessage) - { - Message = message; - Peer = rawMessage.Peer; - PacketReader = rawMessage.PacketReader; - DeliveryMethod = rawMessage.DeliveryMethod; - } -} \ No newline at end of file diff --git a/Source/Reloaded.Messaging/Structs/RawNetMessage.cs b/Source/Reloaded.Messaging/Structs/RawNetMessage.cs deleted file mode 100644 index 7240ccb..0000000 --- a/Source/Reloaded.Messaging/Structs/RawNetMessage.cs +++ /dev/null @@ -1,44 +0,0 @@ -using LiteNetLib; - -namespace Reloaded.Messaging.Structs; - -/// -/// Encapsulates a message received from the network as a raw array of bytes. -/// -public struct RawNetMessage -{ - /// - /// The contents of the message. - /// - public byte[] Message { get; private set; } - - /// - /// The peer from whom the message was received from. - /// - public NetPeer Peer { get; private set; } - - /// - /// Used to read the packet internals, if desired. - /// - public NetPacketReader PacketReader { get; private set; } - - /// - /// The method via which this message was delivered. - /// - public DeliveryMethod DeliveryMethod { get; private set; } - - /// - /// Encapsulates the message received from the network as a raw array of bytes. - /// - /// The message in question. - /// The peer from whom the message was received from. - /// Used to read packet internals, if desired. - /// The method via which the message was delivered. - public RawNetMessage(byte[] message, NetPeer peer, NetPacketReader packetReader, DeliveryMethod deliveryMethod) - { - Message = message; - Peer = peer; - PacketReader = packetReader; - DeliveryMethod = deliveryMethod; - } -} \ No newline at end of file diff --git a/Source/Reloaded.Messaging/Utilities/ArrayRental.cs b/Source/Reloaded.Messaging/Utilities/ArrayRental.cs new file mode 100644 index 0000000..4ffa547 --- /dev/null +++ b/Source/Reloaded.Messaging/Utilities/ArrayRental.cs @@ -0,0 +1,65 @@ +using System; +using System.Buffers; +#if NET5_0_OR_GREATER +using System.Runtime.InteropServices; +#endif + +namespace Reloaded.Messaging.Utilities; + +/// +/// Represents a temporary array rental from the runtime's ArrayPool. +/// +/// Type of element to be rented from the runtime. +public struct ArrayRental : IDisposable +{ + private T[] _data; + private int _count; + + /// + /// Rents an array of bytes from the arraypool. + /// + /// Needed amount of bytes. + public ArrayRental(int count) + { + _data = ArrayPool.Shared.Rent(count); + _count = count; + } + + /// + /// Exposes the raw underlying array, which will likely + /// be bigger than the number of elements. + /// + public T[] RawArray => _data; + + /// + /// Returns the rented array as a span. + /// + public Span Span => SpanExtensions.AsSpanFast(_data, _count); + + /// + /// Exposes the number of elements stored by this structure. + /// + public int Count => _count; + + /// + /// Returns a reference to the first element. + /// + public ref T FirstElement => ref GetFirstElement(); + + /// + /// Returns the array to the pool. + /// + public void Dispose() => ArrayPool.Shared.Return(_data, false); + + /// + /// Returns a reference to the first element. + /// + private ref T GetFirstElement() + { +#if NET5_0 + return ref MemoryMarshal.GetArrayDataReference(_data); +#else + return ref _data[0]; +#endif + } +} \ No newline at end of file diff --git a/Source/Reloaded.Messaging/Utilities/IMessageExtensions.cs b/Source/Reloaded.Messaging/Utilities/IMessageExtensions.cs new file mode 100644 index 0000000..0800a64 --- /dev/null +++ b/Source/Reloaded.Messaging/Utilities/IMessageExtensions.cs @@ -0,0 +1,61 @@ +using Reloaded.Messaging.Interfaces; + +namespace Reloaded.Messaging.Utilities; + +/// +/// Extensions for classes inheriting from +/// +public static class MessageExtensions +{ + /// + /// Creates a reference message handler from an existing message structure.

+ /// Example usage:
+ /// `sample.CreateMessageHandler(new Vector3BrotliReceiveAction(), 0);`
+ /// `sample.CreateMessageHandler(new Vector3BrotliReceiveAction(), default(TExtraData));`

+ /// Where `sample` inherits from IMessage. + ///
+ /// Type of structure used. + /// Type of serializer used. + /// Type of compressor used. + /// Extra data, usually received from network interface etc. + /// Action to add to the reference message handler. + /// The structure containing the message. + /// The action to execute for each message received. + /// Any valid instance of extra data. + /// Dummy parameter for generic type inference. + /// Dummy parameter for generic type inference. + public static ReferenceMessageHandler CreateMessageHandler(this IMessage structure, TMsgRefAction refAction, TExtraData extraDataSample, TSerializer? dummy2 = default, TCompressor? dummy3 = default) + where TSerializer : ISerializer + where TCompressor : ICompressor + where TStruct : IMessage + where TMsgRefAction : IMsgRefAction + { + return new ReferenceMessageHandler((TStruct)structure, refAction); + } + + /// + /// Creates a reference message handler from an existing message structure.

+ /// Example usage:
+ /// `sample.AddToDispatcher(new Vector3BrotliReceiveAction(), ref dispatcher);`

+ /// Where `sample` inherits from IMessage. + ///
+ /// Type of structure used. + /// Type of serializer used. + /// Type of compressor used. + /// Extra data, usually received from network interface etc. + /// Action to add to the reference message handler. + /// The structure containing the message. + /// The action to execute for each message received. + /// The dispatcher to add event handler to. + /// Dummy parameter for generic type inference. + /// Dummy parameter for generic type inference. + public static void AddToDispatcher(this IMessage structure, TMsgRefAction refAction, ref MessageDispatcher dispatcher, TSerializer? dummy2 = default, TCompressor? dummy3 = default) + where TSerializer : ISerializer + where TCompressor : ICompressor + where TStruct : IMessage + where TMsgRefAction : IMsgRefAction + { + var handler = new ReferenceMessageHandler((TStruct)structure, refAction); + dispatcher.AddOrOverrideHandler(handler); + } +} \ No newline at end of file diff --git a/Source/Reloaded.Messaging/Utilities/Pool.cs b/Source/Reloaded.Messaging/Utilities/Pool.cs new file mode 100644 index 0000000..f747808 --- /dev/null +++ b/Source/Reloaded.Messaging/Utilities/Pool.cs @@ -0,0 +1,39 @@ +using Microsoft.IO; +using System; + +namespace Reloaded.Messaging.Utilities; + +internal class Pool +{ + private static RecyclableMemoryStreamManager _recyclableMemoryStreamManager = new(); + + static Pool() => _recyclableMemoryStreamManager.AggressiveBufferReturn = true; + + [ThreadStatic] + private static RecyclableMemoryStream? _compressedMemoryStream; + + [ThreadStatic] + private static RecyclableMemoryStream? _messageMemoryStream; + + internal static RecyclableMemoryStream CompressionStreamPerThread() + { + if (_compressedMemoryStream == null) + { + _compressedMemoryStream = (RecyclableMemoryStream)_recyclableMemoryStreamManager.GetStream(); + return _compressedMemoryStream; + } + + return _compressedMemoryStream; + } + + internal static RecyclableMemoryStream MessageStreamPerThread() + { + if (_messageMemoryStream == null) + { + _messageMemoryStream = (RecyclableMemoryStream)_recyclableMemoryStreamManager.GetStream(); + return _messageMemoryStream; + } + + return _messageMemoryStream; + } +} \ No newline at end of file diff --git a/Source/Reloaded.Messaging/Utilities/SpanExtensions.cs b/Source/Reloaded.Messaging/Utilities/SpanExtensions.cs new file mode 100644 index 0000000..30d4c94 --- /dev/null +++ b/Source/Reloaded.Messaging/Utilities/SpanExtensions.cs @@ -0,0 +1,52 @@ +using System; +using System.Runtime.CompilerServices; +#if NET5_0_OR_GREATER +using System.Runtime.InteropServices; +#endif + +namespace Reloaded.Messaging.Utilities; + +/// +/// Extension methods related to spans. +/// +public static class SpanExtensions +{ + /// + /// Provides zero overhead unsafe array to span conversion. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Span AsSpanFast(T[] data, int length) + { +#if NET5_0_OR_GREATER + return MemoryMarshal.CreateSpan(ref MemoryMarshal.GetArrayDataReference(data), length); +#else + return data.AsSpan(0, length); +#endif + } + + /// + /// Converts a pointer to type T into a byte span with equivalent number of bytes. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe Span AsByteSpanFast(T* data) where T : unmanaged + { +#if NET5_0_OR_GREATER + return MemoryMarshal.CreateSpan(ref Unsafe.AsRef(data), sizeof(T)); +#else + return new Span(data, sizeof(T)); +#endif + } + + /// + /// Slices a span without any bounds checks. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Span SliceFast(Span data, int start) + { +#if NET5_0_OR_GREATER + return MemoryMarshal.CreateSpan(ref Unsafe.Add(ref MemoryMarshal.GetReference(data), start), data.Length - start); +#else + return data.Slice(start); +#endif + } +} \ No newline at end of file diff --git a/docs/benchmarks.md b/docs/benchmarks.md new file mode 100644 index 0000000..0a9acc4 --- /dev/null +++ b/docs/benchmarks.md @@ -0,0 +1,77 @@ +# Sample Benchmarks + +Performed using Version 2.0.0 on the following configuration: + +``` +BenchmarkDotNet=v0.13.1, OS=Windows 10.0.19044.1806 (21H2) +Intel Core i7-4790K CPU 4.00GHz (Haswell), 1 CPU, 8 logical and 4 physical cores +.NET SDK=7.0.100-preview.5.22307.18 + [Host] : .NET 6.0.6 (6.0.622.26707), X64 RyuJIT +``` + +All benchmarks performed on single thread; albeit library is safe to use with multiple threads. + +## Message Unpacker & Dispatcher Benchmarks + +This is a benchmark of `MessageDispatcher`. + +This benchmark measures the time taken to: +- Decode Message Header (Unpack) +- Send Message to Handler for Deserialization + +``` +| Method | NumItems | Mean | Error | StdDev | Ratio | Operations/s | Allocated | +|-------------- |--------- |---------:|----------:|----------:|------:|-------------:|----------:| +| HandleMessage | 100000 | 1.038 ms | 0.0167 ms | 0.0148 ms | 1.00 | 96332329.33 | 2 B | +``` + +Around 96 million messages per second. + +## Message Creation Benchmarks + +This is a benchmark of `MessageWriter.Serialize`. + +This benchmark measures the time taken to: +- Create & Dispose container used for storing packed message. (`Microsoft.IO.RecyclableMemoryStream`) +- Create Message Header. +- Pack Message (combine header + raw data). + +``` +| Method | NumItems | Mean | Error | StdDev | Ratio | RatioSD | Operations/s | Allocated | +|----------------------------------- |--------- |-------------:|-----------:|-----------:|---------:|--------:|--------------:|----------:| +| DummySerializeOnly_BASELINE | 100000 | 13.85 us | 0.062 us | 0.058 us | 1.00 | 0.00 | 7222495640.56 | - | +| DummySerialize_And_Pack | 100000 | 7,294.25 us | 53.658 us | 47.567 us | 526.71 | 4.73 | 13709434.85 | 8 B | +| DummySerialize_And_Pack_Compressed | 100000 | 20,060.07 us | 228.341 us | 213.590 us | 1,448.88 | 17.92 | 4985028.25 | 50 B | +``` + +Key: +- DummySerializeOnly_BASELINE: `Overhead of non-packing related code.` +- DummySerialize_And_Pack: `Time taken to pack all messages without compression.` +- DummySerialize_And_Pack_Compressed: `Time taken to pack all messages with compression header.` + +## Real World Scenario Benchmarks + +The following benchmark measures the time taken to serialize: +- [A real world configuration file](https://github.com/Reloaded-Project/Reloaded-II/blob/32d5e132391d96814ea983cda231c271c43828e0/source/Reloaded.Mod.Loader.IO/Config/ModConfig.cs#L4) into JSON. +- And run it through the library. + +| Method | NumItems | Mean | Error | StdDev | Ratio | RatioSD | Operations/s | Gen 0 | Allocated | +|--------------------------------------------------------- |--------- |---------:|--------:|--------:|------:|--------:|-------------:|-----------:|--------------:| +| SerializeOnly_NoPack_To_SingleBuffer | 100000 | 155.6 ms | 2.76 ms | 2.59 ms | 1.00 | 0.00 | 642735.51 | - | 1,614,672 B | +| SerializeOnly_NoPack_To_BufferPerMessage | 100000 | 205.6 ms | 2.36 ms | 2.21 ms | 1.32 | 0.02 | 486273.70 | 6000.0000 | 26,400,624 B | +| Serialize_And_Pack | 100000 | 157.6 ms | 1.43 ms | 1.34 ms | 1.01 | 0.02 | 634681.18 | - | 252 B | +| Serialize_And_Pack_And_Handle | 100000 | 156.9 ms | 1.97 ms | 1.84 ms | 1.01 | 0.02 | 637216.58 | - | 420 B | +| Serialize_And_Pack_And_Handle_And_Unpack_And_Deserialize | 100000 | 581.8 ms | 7.88 ms | 7.37 ms | 3.74 | 0.06 | 171887.71 | 50000.0000 | 209,719,272 B | + +- SerializeOnly_NoPack_To_SingleBuffer: `Time taken to serialize all messages into a single memory buffer`. +- SerializeOnly_NoPack_To_BufferPerMessage: `Time taken to serialize all messages into 1 memory buffer per message`. +- Serialize_And_Pack: `Time taken to serialize and pack every message using the library.` +- Serialize_And_Pack_And_Handle: `Time taken to serialize, pack and send message via MessageDispatcher.` +- Serialize_And_Pack_And_Handle_And_Unpack_And_Deserialize: `Provided for completeness to measure theoretical throughput.` + +From these results we can extrapolate that: +- Overhead for packing a message is 1-2ms per 100,000 items; (`SerializeOnly_NoPack_To_SingleBuffer` - `Serialize_And_Pack`). + +## Additional Benchmarks + +Further benchmarks can be found in the `Reloaded.Messaging.Benchmarks` project. \ No newline at end of file diff --git a/docs/defining-structures.md b/docs/defining-structures.md new file mode 100644 index 0000000..61292c1 --- /dev/null +++ b/docs/defining-structures.md @@ -0,0 +1,72 @@ +# Creating Structures & Messages + +## Creating Message Structures + +!!! note + + The library heavily (ab)uses generics for optimal code generation and maximum throughput. + +First, a struct or class must be annotated by implementing the `IMessage` interface. + +```csharp +// Structures can specify their own custom serializer(s) and compressor(s). +// IMessage +public struct Vector3 : IMessage, DummyCompressor> +{ + // IMessage + public sbyte GetMessageType() => (sbyte)MessageType.Vector3; + public SystemTextJsonSerializer GetSerializer() => new(); + public DummyCompressor? GetCompressor() => null; + + // Example Data + public float X { get; set; } + public float Y { get; set; } + public float Z { get; set; } +} +``` + +Each message must define the following methods: +- `GetMessageType`: Returns the unique message type. Valid values are between 0 and 127. +- `GetSerializer`: Returns an instance of the serializer used to serialize/deserialize the message. +- `GetCompressor`: Returns an instance of the compressor used to compress/decompress the message. + +The type of serializer (`TSerializer`) and type of compressor (`TCompressor`) are defined in the `IMessage` interface; here they are `SystemTextJsonSerializer` and `DummyCompressor` specifically. + +Return `null` for compressor if no compression is requested. + +## Pack Messages + +To pack an instance (including serialization & compression), call the extension method `Serialize()` in `Reloaded.Messaging.Messages.MessageWriterExtensions`. + +```csharp +var sample = new Vector3(0.0f, 1.0f, 2.0f); +using var serialized = sample.Serialize(ref sample); + +// Access message via `serialized.Span`. +// You can now e.g. send this message over the network. +Client.FirstPeer.Send(serialized.Span, DeliveryMethod.ReliableOrdered); +``` + +!!! danger + + Serialization result must be disposed before serializing another instance due to internal pooling. + The return value `ReusableSingletonMemoryStream` can have at most 1 instance per thread. + +Alternative lower level API: `MessageWriter`. + +## Unpack Messages + +!!! info + + Provided for completeness, this is usually automated for you. + Only low level API provided. + +```csharp +// Read message header. +HeaderReader.ReadHeader(message.Span, out var messageType, out var compressedSize, out var headerSize); + +// Create deserializer & deserialize +// Generic arguments to MessageReader are same as ones to IMessage. +var deserialize = new MessageReader, NullCompressor>(in structure); +Vector3 deserialized = deserialize.Deserialize(message.Span.Slice(headerSize), compressedSize); +``` \ No newline at end of file diff --git a/docs/images/reloaded-icon.png b/docs/images/reloaded-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..13e84a31ce734a4e3b8a8b7de49c4a3dafe03f19 GIT binary patch literal 47501 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 t`V-psw#oRB(n{rOtVV6qFt +

Reloaded: Messaging Module

+ +

+ Assert.Equal(funnyMessage, Dio) +

+ + +# Introduction + +Reloaded.Networking is library that adds support for simple, high performance message packing to existing networking libraries. + +Specifically, it provides a minimal framework for performing the following tasks: + +- Asynchronous message processing for external networking libraries. +- Setting up host & client (for supported libraries). +- Sending/Receiving messages with (de)serialization and [optional] (de)compression. +- Automatically dispatching messages to appropriate handlers (per message type). + +It was originally created for Reloaded II, however has been extended in the hope of becoming a more general purpose library. + +This library is heavily optimized for achieving high throughput for messages `< 128KB`. + +## Characteristics +- High performance. (Memory pooling, low heap allocation). +- Low networking overhead. +- Custom serializer/compressor per class type. +- Simple message packing/protocol. +- 1 byte overhead for uncompressed, 5 bytes for compressed. +- Unsafe. \ No newline at end of file diff --git a/docs/message-structure.md b/docs/message-structure.md new file mode 100644 index 0000000..8f02914 --- /dev/null +++ b/docs/message-structure.md @@ -0,0 +1,50 @@ +# Message Structure + +Each message uses the following structure: + +``` +- [7 bits] Message Type +- [1 bit ] Compression Flag +- [0/4 bytes] Decompressed Size +- [Remaining bytes] Message +``` + +Messages are deliberately simple, to avoid unnecessary complexity. + +## Message Type +This is denoted by `TMessageType` within the library, and is user defined. +Each message type can be assigned 1 handler. + +Type: Signed byte +Values: `0x0 - 0x7F` +Mask: `0b0111_1111` + +## Compression Flag + +Set in leftmost bit of message type. +1 if the message contains compression, else 0. + +Type: Single bit +Values: `0x0 - 0x1` +Mask: `0b1000_0000` + +## Decompressed Size +Expected size after decompression. +Used for compressors to allow for memory pre-allocation. + +Type: Signed 32-bit integer. +Optional: Used only if struct defines a compressor. + +Values: +- `0x0 - 0x7FFFFFFF`: Expected size of decompressed message. + +Uses `Little Endian`. + +The resulting data is always compressed, regardless of whether it ended up being larger or smaller than the original in order to improve throughput (avoids a memory copy). + +Reason for signed vs unsigned lies in limitations of some languages (incl. .NET), memory pools often don't support 4GB arrays. + +## Message + +Raw message in bytes. +How message is stored depends on serializer. \ No newline at end of file diff --git a/docs/running-host.md b/docs/running-host.md new file mode 100644 index 0000000..0f81801 --- /dev/null +++ b/docs/running-host.md @@ -0,0 +1,69 @@ +# Running a Host + +The following article shows how to run an example host. +For a more complete example, look at Unit Tests. + + +## Create a Host + +!!! note + + The hosts in `Reloaded.Messaging` do the minimal amount of setup to enable asynchronous message handling. + They do not abstract the base libraries; for guidance on using them, please refer to their individual documentation(s). + +```csharp +Server = new LiteNetLibHost>(true, new MessageDispatcher()); +Client = new LiteNetLibHost>(true, new MessageDispatcher()); + +// Start listening and connect. +// This is LiteNetLib specific code. +Server.Manager.Start(IPAddress.Loopback, IPAddress.IPv6Loopback, 0); +Client.Manager.Start(IPAddress.Loopback, IPAddress.IPv6Loopback, 0); +Client.Manager.Connect(new IPEndPoint(IPAddress.Loopback, Server.Manager.LocalPort), DefaultPassword); +``` + +All pre-implemented hosts derive from the `IHost` class. +`TExtraData` is a generic type used to encapsulate the current state of the host. +In the case of `LiteNetLib`, it is called `LiteNetLibState`. + +## Sending Messages + +Each host implements the functions `SendFirstPeer` and `SendToAll`. + +Recall the serialization example from earlier: + +```csharp +var sample = new Vector3(0.0f, 1.0f, 2.0f); +using var serialized = sample.Serialize(ref sample); + +// Client is an IHost. +Client.SendFirstPeer(serialized.Span); +``` + +## Receiving Messages + +In order to receive messages, you must first register a handler for the message type. +You can do that by calling `AddToDispatcher` on the message type and providing: +- Class/struct that implements `IMsgRefAction`. +- The `Dispatcher` from the `IHost` instance. + +```csharp +var messageHandler = new LiteNetLibMessageHandler(); +messageHandler.Received.AddToDispatcher(messageHandler, ref Client.Dispatcher); + +// Class that will process received `Vector3`s from Host. +public class LiteNetLibMessageHandler : IMsgRefAction +{ + public void OnMessageReceive(ref Vector3 received, ref LiteNetLibState data) + { + // Executed on every Vector3 received from host. + // e.g. You can process message and send response with + // data.Peer.Send(); + } +} +``` + +Any instance of a class/struct that implements `IMsgRefAction` is fine. +You can of course implement multiple `IMsgRefAction` in the same class/struct too. + +The hosts are automatically configured to asynchronously receive messages, there is no need to manually call `Receive` or any similar function(s). \ No newline at end of file diff --git a/docs/serializers-compressors.md b/docs/serializers-compressors.md new file mode 100644 index 0000000..127bcd5 --- /dev/null +++ b/docs/serializers-compressors.md @@ -0,0 +1,111 @@ +# Serializers & Compressors + +## Existing Serializers + +All NuGet packages are prefixed with `Reloaded.Messaging` unless otherwise specified. + +| Serializer | NuGet Package | Format | Example Use Case | +|---------------------------------------------------------------------|---------------------------|---------|---------------------------------------------------------------------------------------------------| +| SystemTextJsonSerializer
SourceGeneratedSystemTextJsonSerializer | Extras.Runtime | JSON | Human Readable Data | +| MessagePackSerializer | Serializer.MessagePack | MsgPack | High Performance, Small Message Size | +| UnmanagedReloadedMemorySerializer | Serializer.ReloadedMemory | Binary | Raw struct/byte conversion.
When versioning is not needed and client/host use same endian. | + + +## Existing Compressors + +| Compressor | NuGet Package | Example Use Case | +|---------------------|----------------------|------------------------------------------------------------| +| BrotliCompressor | Extras.Runtime | Compressing structured data (e.g. JSON) | +| ZStandardCompressor | Compressor.ZStandard | Binary compression. Very good with pre-trained dictionary. | + +## Implementing your Own + +Implementing serializers and compressors is simple, create structs that implement the `ISerializer` and `ICompressor` interfaces. +Below are some example(s). + +### Example Serializer + +```csharp +/// +public struct SystemTextJsonSerializer : ISerializer +{ + /// + /// Serialization options. + /// + public JsonSerializerOptions Options { get; private set; } + + /// + /// Creates the System.Text.Json based serializer. + /// + public SystemTextJsonSerializer() { Options = new JsonSerializerOptions(); } + + /// + /// Creates the System.Text.Json based serializer. + /// + /// Options to use for serialization/deserialization. + public SystemTextJsonSerializer(JsonSerializerOptions serializerOptions) { Options = serializerOptions; } + + /// + public TStruct Deserialize(Span serialized) + { + return JsonSerializer.Deserialize(serialized, Options)!; + } + + /// + public void Serialize(ref TStruct item, IBufferWriter writer) + { + var write = Pool.JsonWriterPerThread(); + write.Reset(writer); + JsonSerializer.Serialize(write, item, Options); + } +} +``` + +### Example Compressor + +```csharp +/// +/// Provides brotli compression support. +/// +public struct BrotliCompressor : ICompressor +{ + private byte _quality; + private byte _window; + + /// + /// Creates the default brotli compressor. + /// + public BrotliCompressor() + { + _window = 22; + _quality = 9; + } + + /// + /// Quality of encoder. Between 0 and 11. Recommend 9 for size/speed ratio. + /// Size of window. + public BrotliCompressor(byte quality, byte window = 22) + { + _quality = quality; + _window = window; + } + + /// + public int GetMaxCompressedSize(int inputSize) => BrotliEncoder.GetMaxCompressedLength(inputSize); + + /// + public int Compress(Span uncompressedData, Span compressedData) + { + using var encoder = new BrotliEncoder(_quality, _window); + encoder.Compress(uncompressedData, compressedData, out _, out var bytesWritten, true); + return bytesWritten; + } + + /// + public void Decompress(Span compressedBuf, Span uncompressedBuf) + { + using var decoder = new BrotliDecoder(); + decoder.Decompress(compressedBuf, uncompressedBuf, out _, out _); + } +} +``` \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..fa61ac5 --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,50 @@ +site_name: Reloaded.Messaging Documentation +site_url: https://github.com/Reloaded-Project/Reloaded.Messaging + +repo_name: Reloaded-Project/Reloaded.Messaging +repo_url: https://github.com/Reloaded-Project/Reloaded.Messaging + +extra: + social: + - icon: fontawesome/brands/github + link: https://github.com/Sewer56 + - icon: fontawesome/brands/twitter + link: https://twitter.com/TheSewer56 + +markdown_extensions: + - admonition + - tables + - pymdownx.details + - pymdownx.highlight + - pymdownx.superfences + - pymdownx.tasklist + - def_list + - meta + - md_in_html + - attr_list + - footnotes + - pymdownx.emoji: + emoji_index: !!python/name:materialx.emoji.twemoji + emoji_generator: !!python/name:materialx.emoji.to_svg + +plugins: + - search + +theme: + name: material + palette: + primary: red + accent: red + scheme: slate + features: + - navigation.instant + +nav: + - Home: index.md + - Basic Usage: + - Defining Structures: defining-structures.md + - Running A Host: running-host.md + - Serializers & Compressors: serializers-compressors.md + - Benchmarks: benchmarks.md + - Internals: + - Message Structure: message-structure.md \ No newline at end of file From 726d8086b8f12c0395fe37b01e35d35da312fa3f Mon Sep 17 00:00:00 2001 From: Sewer 56 Date: Fri, 8 Jul 2022 23:56:44 +0100 Subject: [PATCH 13/16] Added: Assembly Trimming for Supported Libraries --- .../Reloaded.Messaging.Benchmarks.csproj | 5 ++- ...aded.Messaging.Compressor.ZStandard.csproj | 3 ++ .../Reloaded.Messaging.Extras.Runtime.csproj | 5 ++- .../SystemTextJsonSerializer.cs | 13 ++++++-- .../Reloaded.Messaging.Host.LiteNetLib.csproj | 3 ++ ...ed.Messaging.Serializer.MessagePack.csproj | 4 ++- ...Messaging.Serializer.ReloadedMemory.csproj | 3 ++ Source/Reloaded.Messaging.Trimming/Program.cs | 2 ++ .../Reloaded.Messaging.Trimming.csproj | 32 +++++++++++++++++++ Source/Reloaded.Messaging.sln | 11 +++++-- .../Reloaded.Messaging.csproj | 3 ++ 11 files changed, 77 insertions(+), 7 deletions(-) create mode 100644 Source/Reloaded.Messaging.Trimming/Program.cs create mode 100644 Source/Reloaded.Messaging.Trimming/Reloaded.Messaging.Trimming.csproj diff --git a/Source/Reloaded.Messaging.Benchmarks/Reloaded.Messaging.Benchmarks.csproj b/Source/Reloaded.Messaging.Benchmarks/Reloaded.Messaging.Benchmarks.csproj index 6f1dc02..d7bb76a 100644 --- a/Source/Reloaded.Messaging.Benchmarks/Reloaded.Messaging.Benchmarks.csproj +++ b/Source/Reloaded.Messaging.Benchmarks/Reloaded.Messaging.Benchmarks.csproj @@ -1,4 +1,4 @@ - + Exe @@ -16,9 +16,12 @@ + + + diff --git a/Source/Reloaded.Messaging.Compressor.ZStandard/Reloaded.Messaging.Compressor.ZStandard.csproj b/Source/Reloaded.Messaging.Compressor.ZStandard/Reloaded.Messaging.Compressor.ZStandard.csproj index a2251c1..c61ff8b 100644 --- a/Source/Reloaded.Messaging.Compressor.ZStandard/Reloaded.Messaging.Compressor.ZStandard.csproj +++ b/Source/Reloaded.Messaging.Compressor.ZStandard/Reloaded.Messaging.Compressor.ZStandard.csproj @@ -13,6 +13,9 @@ 2.0.0 true NuGet-Icon.png + + true + true diff --git a/Source/Reloaded.Messaging.Extras.Runtime/Reloaded.Messaging.Extras.Runtime.csproj b/Source/Reloaded.Messaging.Extras.Runtime/Reloaded.Messaging.Extras.Runtime.csproj index 5262553..18c4040 100644 --- a/Source/Reloaded.Messaging.Extras.Runtime/Reloaded.Messaging.Extras.Runtime.csproj +++ b/Source/Reloaded.Messaging.Extras.Runtime/Reloaded.Messaging.Extras.Runtime.csproj @@ -1,7 +1,7 @@  - netcoreapp3.1;net6.0 + netcoreapp3.1;net6.0;net5.0 preview Sewer56 Sewer56 @@ -16,6 +16,9 @@ NuGet-Icon.png enable true + + true + true diff --git a/Source/Reloaded.Messaging.Extras.Runtime/SystemTextJsonSerializer.cs b/Source/Reloaded.Messaging.Extras.Runtime/SystemTextJsonSerializer.cs index 8641027..f3dd7bf 100644 --- a/Source/Reloaded.Messaging.Extras.Runtime/SystemTextJsonSerializer.cs +++ b/Source/Reloaded.Messaging.Extras.Runtime/SystemTextJsonSerializer.cs @@ -1,5 +1,6 @@ using System; using System.Buffers; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Runtime.CompilerServices; using System.Text.Json; @@ -8,7 +9,11 @@ namespace Reloaded.Messaging.Extras.Runtime; /// -public struct SystemTextJsonSerializer : ISerializer +public struct SystemTextJsonSerializer< +#if NET5_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] +#endif +TStruct> : ISerializer { /// /// Serialization options. @@ -35,15 +40,19 @@ public SystemTextJsonSerializer(JsonSerializerOptions serializerOptions) /// #if NET5_0_OR_GREATER [SkipLocalsInit] + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026", Justification = "Types passed to this serializer are preserved.")] #endif public TStruct Deserialize(Span serialized) { - return JsonSerializer.Deserialize(serialized, Options)!; + return JsonSerializer.Deserialize< + + TStruct > (serialized, Options)!; } /// #if NET5_0_OR_GREATER [SkipLocalsInit] + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026", Justification = "Types passed to this serializer are preserved.")] #endif public void Serialize(ref TStruct item, IBufferWriter writer) { diff --git a/Source/Reloaded.Messaging.Host.LiteNetLib/Reloaded.Messaging.Host.LiteNetLib.csproj b/Source/Reloaded.Messaging.Host.LiteNetLib/Reloaded.Messaging.Host.LiteNetLib.csproj index fd928d0..8287279 100644 --- a/Source/Reloaded.Messaging.Host.LiteNetLib/Reloaded.Messaging.Host.LiteNetLib.csproj +++ b/Source/Reloaded.Messaging.Host.LiteNetLib/Reloaded.Messaging.Host.LiteNetLib.csproj @@ -19,6 +19,9 @@ true NuGet-Icon.png enable + + true + true diff --git a/Source/Reloaded.Messaging.Serializer.MessagePack/Reloaded.Messaging.Serializer.MessagePack.csproj b/Source/Reloaded.Messaging.Serializer.MessagePack/Reloaded.Messaging.Serializer.MessagePack.csproj index 9ff58b8..26349b2 100644 --- a/Source/Reloaded.Messaging.Serializer.MessagePack/Reloaded.Messaging.Serializer.MessagePack.csproj +++ b/Source/Reloaded.Messaging.Serializer.MessagePack/Reloaded.Messaging.Serializer.MessagePack.csproj @@ -16,8 +16,10 @@ NuGet-Icon.png true enable - + false + true + diff --git a/Source/Reloaded.Messaging.Serializer.ReloadedMemory/Reloaded.Messaging.Serializer.ReloadedMemory.csproj b/Source/Reloaded.Messaging.Serializer.ReloadedMemory/Reloaded.Messaging.Serializer.ReloadedMemory.csproj index 51b1230..0e9bed1 100644 --- a/Source/Reloaded.Messaging.Serializer.ReloadedMemory/Reloaded.Messaging.Serializer.ReloadedMemory.csproj +++ b/Source/Reloaded.Messaging.Serializer.ReloadedMemory/Reloaded.Messaging.Serializer.ReloadedMemory.csproj @@ -14,6 +14,9 @@ true NuGet-Icon.png true + + true + true diff --git a/Source/Reloaded.Messaging.Trimming/Program.cs b/Source/Reloaded.Messaging.Trimming/Program.cs new file mode 100644 index 0000000..3751555 --- /dev/null +++ b/Source/Reloaded.Messaging.Trimming/Program.cs @@ -0,0 +1,2 @@ +// See https://aka.ms/new-console-template for more information +Console.WriteLine("Hello, World!"); diff --git a/Source/Reloaded.Messaging.Trimming/Reloaded.Messaging.Trimming.csproj b/Source/Reloaded.Messaging.Trimming/Reloaded.Messaging.Trimming.csproj new file mode 100644 index 0000000..18b39e0 --- /dev/null +++ b/Source/Reloaded.Messaging.Trimming/Reloaded.Messaging.Trimming.csproj @@ -0,0 +1,32 @@ + + + + Exe + net6.0 + enable + enable + + + true + link + false + + + + + + + + + + + + + + + + + + + + diff --git a/Source/Reloaded.Messaging.sln b/Source/Reloaded.Messaging.sln index 0508c65..bc6f361 100644 --- a/Source/Reloaded.Messaging.sln +++ b/Source/Reloaded.Messaging.sln @@ -17,11 +17,13 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Reloaded.Messaging.Interfac EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Reloaded.Messaging.Extras.Runtime", "Reloaded.Messaging.Extras.Runtime\Reloaded.Messaging.Extras.Runtime.csproj", "{E80F0107-A3A7-4DDF-BA5F-3C771F54062E}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Reloaded.Messaging.Host.LiteNetLib", "Reloaded.Messaging.Host.LiteNetLib\Reloaded.Messaging.Host.LiteNetLib.csproj", "{1189FFF2-616A-478D-99C7-C1FDC068413C}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Reloaded.Messaging.Host.LiteNetLib", "Reloaded.Messaging.Host.LiteNetLib\Reloaded.Messaging.Host.LiteNetLib.csproj", "{1189FFF2-616A-478D-99C7-C1FDC068413C}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Extra", "Extra", "{73B9A805-15CC-4DE4-8039-7FEBB366D4E1}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Reloaded.Messaging.Benchmarks", "Reloaded.Messaging.Benchmarks\Reloaded.Messaging.Benchmarks.csproj", "{05BAA584-FEFF-4180-8035-6F075A17A051}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Reloaded.Messaging.Benchmarks", "Reloaded.Messaging.Benchmarks\Reloaded.Messaging.Benchmarks.csproj", "{05BAA584-FEFF-4180-8035-6F075A17A051}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Reloaded.Messaging.Trimming", "Reloaded.Messaging.Trimming\Reloaded.Messaging.Trimming.csproj", "{A35E3C2B-2D4B-4CCB-9E6C-47C15561B5F7}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -65,12 +67,17 @@ Global {05BAA584-FEFF-4180-8035-6F075A17A051}.Debug|Any CPU.Build.0 = Debug|Any CPU {05BAA584-FEFF-4180-8035-6F075A17A051}.Release|Any CPU.ActiveCfg = Release|Any CPU {05BAA584-FEFF-4180-8035-6F075A17A051}.Release|Any CPU.Build.0 = Release|Any CPU + {A35E3C2B-2D4B-4CCB-9E6C-47C15561B5F7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A35E3C2B-2D4B-4CCB-9E6C-47C15561B5F7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A35E3C2B-2D4B-4CCB-9E6C-47C15561B5F7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A35E3C2B-2D4B-4CCB-9E6C-47C15561B5F7}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution {05BAA584-FEFF-4180-8035-6F075A17A051} = {73B9A805-15CC-4DE4-8039-7FEBB366D4E1} + {A35E3C2B-2D4B-4CCB-9E6C-47C15561B5F7} = {73B9A805-15CC-4DE4-8039-7FEBB366D4E1} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {E6F63326-4EEA-4D4E-BB44-60FD2065CB3E} diff --git a/Source/Reloaded.Messaging/Reloaded.Messaging.csproj b/Source/Reloaded.Messaging/Reloaded.Messaging.csproj index bee2d7d..3f066a9 100644 --- a/Source/Reloaded.Messaging/Reloaded.Messaging.csproj +++ b/Source/Reloaded.Messaging/Reloaded.Messaging.csproj @@ -20,6 +20,9 @@ true NuGet-Icon.png enable + + false + true From 4b2a3f7a6ec714496eb46be71271bca0264c5b48 Mon Sep 17 00:00:00 2001 From: Sewer 56 Date: Sat, 9 Jul 2022 00:04:53 +0100 Subject: [PATCH 14/16] Added: Basic CI/CD --- .github/workflows/build-and-publish.yml | 128 ++++++++++++++++++++++++ .github/workflows/deploy-mkdocs.yml | 32 ++++++ 2 files changed, 160 insertions(+) create mode 100644 .github/workflows/build-and-publish.yml create mode 100644 .github/workflows/deploy-mkdocs.yml diff --git a/.github/workflows/build-and-publish.yml b/.github/workflows/build-and-publish.yml new file mode 100644 index 0000000..bb05f92 --- /dev/null +++ b/.github/workflows/build-and-publish.yml @@ -0,0 +1,128 @@ +name: Build and Publish + +on: + push: + branches: [ master, 2.X ] + tags: + - '*' + pull_request: + branches: [ master, 2.X ] + workflow_dispatch: + +env: + CHANGELOG_PATH: ./Changelog.md + CODE_COVERAGE_PATH: ./Coverage.xml + SOLUTION_PATH: ./Source/Reloaded.Messaging.sln + NUPKG_GLOB: ./Source/**/*.nupkg + IS_RELEASE: ${{ startsWith(github.ref, 'refs/tags/') }} + RELEASE_TAG: ${{ github.ref_name }} + +jobs: + build: + runs-on: windows-2022 + defaults: + run: + shell: pwsh + + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - name: Setup .NET Core SDK (3.1.x) + uses: actions/setup-dotnet@v1.9.0 + with: + # Optional SDK version(s) to use. If not provided, will install global.json version when available. Examples: 2.2.104, 3.1, 3.1.x + dotnet-version: 3.1.x + + - name: Setup .NET Core SDK (5.0.x) + uses: actions/setup-dotnet@v1.9.0 + with: + # Optional SDK version(s) to use. If not provided, will install global.json version when available. Examples: 2.2.104, 3.1, 3.1.x + dotnet-version: 5.0.x + + - name: Setup .NET Core SDK (6.0.x) + uses: actions/setup-dotnet@v1.9.0 + with: + # Optional SDK version(s) to use. If not provided, will install global.json version when available. Examples: 2.2.104, 3.1, 3.1.x + dotnet-version: 6.0.x + + # Required for C#10 features. + - name: Setup Node.js + uses: actions/setup-node@v2 + with: + node-version: '14' + + - name: Setup AutoChangelog + run: npm install -g auto-changelog + + - name: Get Dotnet Info + run: dotnet --info + + - name: Build + run: dotnet build -c Release "$env:SOLUTION_PATH" + + - name: Test + run: | + dotnet test ./Source/Reloaded.Messaging.Tests/Reloaded.Messaging.Tests.csproj /p:CollectCoverage=true /p:CoverletOutputFormat=opencover /p:CoverletOutput="../../$env:CODE_COVERAGE_PATH" --% /p:Exclude=\"[xunit.*]*\" + + - name: Codecov + # You may pin to the exact commit or the version. + # uses: codecov/codecov-action@f32b3a3741e1053eb607407145bc9619351dc93b + uses: codecov/codecov-action@v2.1.0 + with: + # Comma-separated list of files to upload + files: ${{ env.CODE_COVERAGE_PATH }} + + - name: Create Changelog (on Tag) + run: | + if ($env:IS_RELEASE -eq 'true') + { + auto-changelog --sort-commits date --hide-credit --template keepachangelog --commit-limit false --unreleased --starting-version "$env:RELEASE_TAG" --output "$env:CHANGELOG_PATH" + } + else + { + auto-changelog --sort-commits date --hide-credit --template keepachangelog --commit-limit false --unreleased --output "$env:CHANGELOG_PATH" + } + + - name: Upload NuGet Package Artifact + uses: actions/upload-artifact@v2.2.4 + with: + # Artifact name + name: NuGet Packages + # A file, directory or wildcard pattern that describes what to upload + path: | + ${{ env.NUPKG_GLOB }} + + - name: Upload Changelog Artifact + uses: actions/upload-artifact@v2.2.4 + with: + # Artifact name + name: Changelog + # A file, directory or wildcard pattern that describes what to upload + path: ${{ env.CHANGELOG_PATH }} + retention-days: 0 + + + - name: Upload to GitHub Releases + uses: softprops/action-gh-release@v0.1.14 + if: env.IS_RELEASE == 'true' + with: + # Path to load note-worthy description of changes in release from + body_path: ${{ env.CHANGELOG_PATH }} + # Newline-delimited list of path globs for asset files to upload + files: | + ${{ env.NUPKG_GLOB }} + ${{ env.CHANGELOG_PATH }} + + - name: Upload to NuGet (on Tag) + env: + NUGET_KEY: ${{ secrets.NUGET_KEY }} + if: env.IS_RELEASE == 'true' + run: | + $items = Get-ChildItem -Path "$env:NUPKG_GLOB" -Recurse + Foreach ($item in $items) + { + Write-Host "Pushing $item" + dotnet nuget push "$item" -k "$env:NUGET_KEY" -s "https://api.nuget.org/v3/index.json" --skip-duplicate + } \ No newline at end of file diff --git a/.github/workflows/deploy-mkdocs.yml b/.github/workflows/deploy-mkdocs.yml new file mode 100644 index 0000000..7a2567a --- /dev/null +++ b/.github/workflows/deploy-mkdocs.yml @@ -0,0 +1,32 @@ +name: DeployMkDocs + +# Controls when the action will run. +on: + # Triggers the workflow on push on the master branch + push: + branches: [ master, 2.X ] + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +# A workflow run is made up of one or more jobs that can run sequentially or in parallel +jobs: + # This workflow contains a single job called "build" + build: + # The type of runner that the job will run on + runs-on: ubuntu-latest + + # Steps represent a sequence of tasks that will be executed as part of the job + steps: + + # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it + - name: Checkout Branch + uses: actions/checkout@v2 + + # Deploy MkDocs + - name: Deploy MkDocs + # You may pin to the exact commit or the version. + # uses: mhausenblas/mkdocs-deploy-gh-pages@66340182cb2a1a63f8a3783e3e2146b7d151a0bb + uses: mhausenblas/mkdocs-deploy-gh-pages@master + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file From ec80fe64687497c5da814726424fe1cba7731abc Mon Sep 17 00:00:00 2001 From: Sewer 56 Date: Sat, 9 Jul 2022 00:07:10 +0100 Subject: [PATCH 15/16] Updated: Readme to link to docs. --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 517d9da..56241f9 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ It was originally created for Reloaded II, however has been extended in the hope This library is heavily optimized for achieving high throughput for messages `< 128KB`. ## Characteristics -- High performance. (Memory pooling, low heap allocation, stack allocation). +- High performance. (Memory pooling, allocation free). - Low networking overhead. - Custom serializer/compressor per class type. - Simple message packing/protocol. @@ -30,4 +30,4 @@ This library is heavily optimized for achieving high throughput for messages `< ## Documentation -More information can be found in the dedicated documentation site. \ No newline at end of file +More information can be found in the [dedicated documentation site](https://reloaded-project.github.io/Reloaded.Messaging). \ No newline at end of file From 40be8391262d3f5daeec7f3779f4c68e978ddf4c Mon Sep 17 00:00:00 2001 From: Sewer 56 Date: Sat, 9 Jul 2022 00:11:46 +0100 Subject: [PATCH 16/16] Updated: I forgot how slow Windows GitHub Runner is --- Source/Reloaded.Messaging.Tests/LiteNetLibHostTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Reloaded.Messaging.Tests/LiteNetLibHostTests.cs b/Source/Reloaded.Messaging.Tests/LiteNetLibHostTests.cs index 013bf6a..f19e499 100644 --- a/Source/Reloaded.Messaging.Tests/LiteNetLibHostTests.cs +++ b/Source/Reloaded.Messaging.Tests/LiteNetLibHostTests.cs @@ -37,7 +37,7 @@ public void Dispose() Client?.Dispose(); } - [Fact(Timeout = 5000)] + [Fact(Timeout = 20000)] public async Task SendAndReceiveMessage() { // Arrange/Setup Host