From 9f2a79152522d8bc41333808bb84369ae723d5fb Mon Sep 17 00:00:00 2001
From: Leonid Umanskiy <me@leonidumanskiy.com>
Date: Tue, 30 Jan 2024 09:12:08 +0100
Subject: [PATCH] Minor refactor of the network protocol. Fixed issue when the
 same message could not be used as an event, request, or response at the same
 time.

---
 .../Integration/IntegrationTests.cs           | 100 ++++++++++++++++++
 .../Unit/Network/MessageReaderTests.cs        |  84 ++++++++++++---
 .../Unit/Network/MessageWriterTests.cs        |  84 ++++++++++++---
 .../Fenrir.Multiplayer.csproj                 |   2 +-
 .../Runtime/LiteNet/LiteNetClientPeer.cs      |   7 +-
 .../LiteNet/LiteNetProtocolListener.cs        |   2 +-
 .../Runtime/LiteNet/LiteNetServerPeer.cs      |   2 +-
 .../Assets/Runtime/Network/MessageFlags.cs    |  14 +--
 .../Assets/Runtime/Network/MessageReader.cs   |  40 ++++---
 .../Assets/Runtime/Network/MessageType.cs     |  27 +++--
 .../Assets/Runtime/Network/MessageWrapper.cs  |  21 +++-
 .../Assets/Runtime/Network/MessageWriter.cs   |  11 +-
 source/UnityPackage/Assets/package.json       |   2 +-
 13 files changed, 311 insertions(+), 85 deletions(-)

diff --git a/source/Fenrir.Multiplayer.Tests/Integration/IntegrationTests.cs b/source/Fenrir.Multiplayer.Tests/Integration/IntegrationTests.cs
index cb35d1f..f22d29c 100644
--- a/source/Fenrir.Multiplayer.Tests/Integration/IntegrationTests.cs
+++ b/source/Fenrir.Multiplayer.Tests/Integration/IntegrationTests.cs
@@ -549,6 +549,60 @@ await Assert.ThrowsExceptionAsync<RequestTimeoutException>(async () =>
             });
         }
 
+        [TestMethod, Timeout(TestTimeout)]
+        public async Task NetworkClient_SendRequest_CanSendMessageThatImplementsEventRequestResponse()
+        {
+            using var logger = new TestLogger();
+            using var networkServer = new NetworkServer(logger);
+
+            TaskCompletionSource<TestMessage> requestTcs = new TaskCompletionSource<TestMessage>();
+            networkServer.AddRequestHandler(new TcsRequestHandler<TestMessage>(requestTcs));
+
+            networkServer.Start();
+
+            Assert.AreEqual(ServerStatus.Running, networkServer.Status, "server is not running");
+
+            using var networkClient = new NetworkClient(logger);
+            var connectionResponse = await networkClient.Connect("http://127.0.0.1:27016");
+
+            Assert.AreEqual(ConnectionState.Connected, networkClient.State, "client is connected");
+            Assert.IsTrue(connectionResponse.Success, "connection rejected");
+
+            networkClient.Peer.SendRequest(new TestMessage() { Value = "test_value" });
+
+            TestMessage request = await requestTcs.Task;
+
+            Assert.AreEqual(request.Value, "test_value");
+        }
+
+
+        [TestMethod, Timeout(TestTimeout)]
+        public async Task NetworkClient_SendRequestResponse_CanSendMessageThatImplementsEventRequestResponse()
+        {
+            using var logger = new TestLogger();
+            using var networkServer = new NetworkServer(logger);
+
+            networkServer.AddRequestHandlerAsync(new TestAsyncRequestResponseHandler<TestMessage, TestMessage>(request =>
+            {
+                Assert.AreEqual("test", request.Value);
+                return Task.FromResult(new TestMessage() { Value = request.Value });
+            }));
+
+            networkServer.Start();
+
+            Assert.AreEqual(ServerStatus.Running, networkServer.Status, "server is not running");
+
+            using var networkClient = new NetworkClient(logger);
+            var connectionResponse = await networkClient.Connect("http://127.0.0.1:27016");
+
+            Assert.AreEqual(ConnectionState.Connected, networkClient.State, "client is not connected");
+            Assert.IsTrue(connectionResponse.Success, "connection rejected");
+
+            var response = await networkClient.Peer.SendRequest<TestMessage, TestMessage>(new TestMessage() { Value = "test" });
+
+            Assert.AreEqual(response.Value, "test");
+        }
+
         [TestMethod, Timeout(TestTimeout)]
         public async Task NetworkServer_SendEvent_SendsEvent()
         {
@@ -579,6 +633,37 @@ public async Task NetworkServer_SendEvent_SendsEvent()
             Assert.AreEqual(testEvent.Value, "event_test");
         }
 
+
+        [TestMethod, Timeout(TestTimeout)]
+        public async Task NetworkServer_SendEvent_CanSendMessageThatImplementsEventRequestResponse()
+        {
+            using var logger = new TestLogger();
+            using var networkServer = new NetworkServer(logger);
+
+            networkServer.PeerConnected += (sender, e) =>
+            {
+                e.Peer.SendEvent(new TestMessage() { Value = "test" });
+            };
+            networkServer.Start();
+
+            Assert.AreEqual(ServerStatus.Running, networkServer.Status, "server is not running");
+
+            TaskCompletionSource<TestMessage> tcs = new TaskCompletionSource<TestMessage>();
+
+            using var networkClient = new NetworkClient(logger);
+            var eventHandler = new TestEventHandler<TestMessage>(tcs);
+            networkClient.AddEventHandler<TestMessage>(eventHandler);
+
+            var connectionResponse = await networkClient.Connect("http://127.0.0.1:27016");
+
+            Assert.AreEqual(ConnectionState.Connected, networkClient.State, "client is not connected");
+            Assert.IsTrue(connectionResponse.Success, "connection rejected");
+
+            var testEvent = await tcs.Task;
+
+            Assert.AreEqual(testEvent.Value, "test");
+        }
+
         [TestMethod, Timeout(TestTimeout)]
         public async Task NetworkServer_Peers_IncludesConnectedPeer()
         {
@@ -882,6 +967,21 @@ public void Serialize(IByteStreamWriter writer)
             }
         }
 
+        class TestMessage : IEvent, IRequest, IRequest<TestMessage>, IResponse, IByteStreamSerializable
+        {
+            public string Value;
+
+            public void Deserialize(IByteStreamReader reader)
+            {
+                Value = reader.ReadString();
+            }
+
+            public void Serialize(IByteStreamWriter writer)
+            {
+                writer.Write(Value);
+            }
+        }
+
         class TestEventHandler<TEvent> : IEventHandler<TEvent>
             where TEvent : IEvent
         {
diff --git a/source/Fenrir.Multiplayer.Tests/Unit/Network/MessageReaderTests.cs b/source/Fenrir.Multiplayer.Tests/Unit/Network/MessageReaderTests.cs
index 795c22c..38e2f4e 100644
--- a/source/Fenrir.Multiplayer.Tests/Unit/Network/MessageReaderTests.cs
+++ b/source/Fenrir.Multiplayer.Tests/Unit/Network/MessageReaderTests.cs
@@ -6,7 +6,7 @@ namespace Fenrir.Multiplayer.Tests.Unit.LiteNetProtocol
     public class MessageReaderTests
     {
         // Message format: 
-        // 1. [1 byte flags]
+        // 1. [1 byte message type + flags]
         // 2. [8 bytes long message type hash]
         // 3. [1 byte channel number]
         // 4. [2 bytes short requestId] - optional, if flags has HasRequestId
@@ -22,7 +22,11 @@ public void MessageReader_TryReadMessage_ReadsEvent()
 
             // Write test data
             var byteStreamWriter = new ByteStreamWriter(serializer);
-            byteStreamWriter.Write((byte)MessageFlags.IsEncrypted); // byte flags
+
+            byte typeAndFlagsCombined = (byte)MessageType.Event;
+            typeAndFlagsCombined = (byte)(typeAndFlagsCombined << 5);
+            typeAndFlagsCombined = (byte)(typeAndFlagsCombined | (byte)MessageFlags.IsEncrypted);
+            byteStreamWriter.Write(typeAndFlagsCombined); // byte type + flags
             byteStreamWriter.Write(typeHashMap.GetTypeHash<TestEvent>()); // ulong type hash
             byteStreamWriter.Write((byte)123); // byte Channel number
             serializer.Serialize(new TestEvent() { Value = "test" }, byteStreamWriter); // byte[] data
@@ -37,7 +41,6 @@ public void MessageReader_TryReadMessage_ReadsEvent()
             Assert.AreEqual("test", ((TestEvent)messageWrapper.MessageData).Value);
             Assert.AreEqual(123, messageWrapper.Channel);
             Assert.IsTrue(messageWrapper.Flags.HasFlag(MessageFlags.IsEncrypted));
-            Assert.IsFalse(messageWrapper.Flags.HasFlag(MessageFlags.HasRequestId));
         }
 
         [TestMethod]
@@ -49,7 +52,10 @@ public void MessageReader_TryReadMessage_ReadsEvent_WhenEmptyData()
 
             // Write test data
             var byteStreamWriter = new ByteStreamWriter(serializer);
-            byteStreamWriter.Write((byte)MessageFlags.IsEncrypted); // byte flags
+            byte typeAndFlagsCombined = (byte)MessageType.Event;
+            typeAndFlagsCombined = (byte)(typeAndFlagsCombined << 5);
+            typeAndFlagsCombined = (byte)(typeAndFlagsCombined | (byte)MessageFlags.IsEncrypted);
+            byteStreamWriter.Write(typeAndFlagsCombined); // byte type + flags
             byteStreamWriter.Write(typeHashMap.GetTypeHash<TestEmptyEvent>()); // ulong type hash
             byteStreamWriter.Write((byte)123); // byte Channel number
             serializer.Serialize(new TestEmptyEvent(), byteStreamWriter); // byte[] data
@@ -63,7 +69,6 @@ public void MessageReader_TryReadMessage_ReadsEvent_WhenEmptyData()
             Assert.IsInstanceOfType(messageWrapper.MessageData, typeof(TestEmptyEvent));
             Assert.AreEqual(123, messageWrapper.Channel);
             Assert.IsTrue(messageWrapper.Flags.HasFlag(MessageFlags.IsEncrypted));
-            Assert.IsFalse(messageWrapper.Flags.HasFlag(MessageFlags.HasRequestId));
         }
 
         [TestMethod]
@@ -75,10 +80,12 @@ public void MessageReader_TryReadMessage_ReadsRequest()
 
             // Write test data
             var byteStreamWriter = new ByteStreamWriter(serializer);
-            byteStreamWriter.Write((byte)(MessageFlags.IsEncrypted | MessageFlags.HasRequestId)); // byte flags
+            byte typeAndFlagsCombined = (byte)MessageType.Request;
+            typeAndFlagsCombined = (byte)(typeAndFlagsCombined << 5);
+            typeAndFlagsCombined = (byte)(typeAndFlagsCombined | (byte)MessageFlags.IsEncrypted);
+            byteStreamWriter.Write(typeAndFlagsCombined); // byte type + flags
             byteStreamWriter.Write(typeHashMap.GetTypeHash<TestRequest>()); // [ulong] type hash
             byteStreamWriter.Write((byte)123); // byte Channel number
-            byteStreamWriter.Write((short)456); // short Request id
             serializer.Serialize(new TestRequest() { Value = "test" }, byteStreamWriter); // data
 
             // Read message
@@ -88,9 +95,38 @@ public void MessageReader_TryReadMessage_ReadsRequest()
             Assert.IsTrue(result);
             Assert.AreEqual(MessageType.Request, messageWrapper.MessageType);
             Assert.AreEqual(123, messageWrapper.Channel);
+            Assert.AreEqual(true, messageWrapper.Flags.HasFlag(MessageFlags.IsEncrypted));
+            Assert.IsInstanceOfType(messageWrapper.MessageData, typeof(TestRequest));
+            Assert.AreEqual("test", ((TestRequest)messageWrapper.MessageData).Value);
+        }
+
+        [TestMethod]
+        public void MessageReader_TryReadMessage_ReadsRequestWithResponse()
+        {
+            var typeHashMap = new TypeHashMap();
+            var serializer = new NetworkSerializer();
+            var messageReader = new MessageReader(serializer, typeHashMap, new EventBasedLogger(), new RecyclableObjectPool<ByteStreamReader>(() => new ByteStreamReader(serializer)));
+
+            // Write test data
+            var byteStreamWriter = new ByteStreamWriter(serializer);
+            byte typeAndFlagsCombined = (byte)MessageType.RequestWithResponse;
+            typeAndFlagsCombined = (byte)(typeAndFlagsCombined << 5);
+            typeAndFlagsCombined = (byte)(typeAndFlagsCombined | (byte)MessageFlags.IsEncrypted);
+            byteStreamWriter.Write(typeAndFlagsCombined); // byte type + flags
+            byteStreamWriter.Write(typeHashMap.GetTypeHash<TestRequest>()); // [ulong] type hash
+            byteStreamWriter.Write((byte)123); // byte Channel number
+            byteStreamWriter.Write((short)456); // short Request id
+            serializer.Serialize(new TestRequest() { Value = "test" }, byteStreamWriter); // data
+
+            // Read message
+            var byteStreamReader = new ByteStreamReader(byteStreamWriter, serializer);
+            bool result = messageReader.TryReadMessage(byteStreamReader, out MessageWrapper messageWrapper);
+
+            Assert.IsTrue(result);
+            Assert.AreEqual(MessageType.RequestWithResponse, messageWrapper.MessageType);
+            Assert.AreEqual(123, messageWrapper.Channel);
             Assert.AreEqual(456, messageWrapper.RequestId);
             Assert.AreEqual(true, messageWrapper.Flags.HasFlag(MessageFlags.IsEncrypted));
-            Assert.AreEqual(true, messageWrapper.Flags.HasFlag(MessageFlags.HasRequestId));
             Assert.IsInstanceOfType(messageWrapper.MessageData, typeof(TestRequest));
             Assert.AreEqual("test", ((TestRequest)messageWrapper.MessageData).Value);
         }
@@ -104,7 +140,10 @@ public void MessageReader_TryReadMessage_ReadsResponse()
 
             // Write test data
             var byteStreamWriter = new ByteStreamWriter(serializer);
-            byteStreamWriter.Write((byte)(MessageFlags.IsEncrypted | MessageFlags.HasRequestId)); // byte flags
+            byte typeAndFlagsCombined = (byte)MessageType.Response;
+            typeAndFlagsCombined = (byte)(typeAndFlagsCombined << 5);
+            typeAndFlagsCombined = (byte)(typeAndFlagsCombined | (byte)MessageFlags.IsEncrypted);
+            byteStreamWriter.Write(typeAndFlagsCombined); // byte type + flags
             byteStreamWriter.Write(typeHashMap.GetTypeHash<TestResponse>()); // [ulong] type hash
             byteStreamWriter.Write((byte)123); // byte Channel number
             byteStreamWriter.Write((short)456); // short Request id
@@ -119,7 +158,6 @@ public void MessageReader_TryReadMessage_ReadsResponse()
             Assert.AreEqual(123, messageWrapper.Channel);
             Assert.AreEqual(456, messageWrapper.RequestId);
             Assert.AreEqual(true, messageWrapper.Flags.HasFlag(MessageFlags.IsEncrypted));
-            Assert.AreEqual(true, messageWrapper.Flags.HasFlag(MessageFlags.HasRequestId));
             Assert.IsInstanceOfType(messageWrapper.MessageData, typeof(TestResponse));
             Assert.AreEqual("test", ((TestResponse)messageWrapper.MessageData).Value);
         }
@@ -150,7 +188,10 @@ public void MessageReader_TryReadMessage_ReturnsFalse_IfMissingMessageTypeHash()
 
             // Write test data
             var byteStreamWriter = new ByteStreamWriter(serializer);
-            byteStreamWriter.Write((byte)(MessageFlags.IsEncrypted | MessageFlags.HasRequestId)); // byte flags
+            byte typeAndFlagsCombined = (byte)MessageType.Event;
+            typeAndFlagsCombined = (byte)(typeAndFlagsCombined << 5);
+            typeAndFlagsCombined = (byte)(typeAndFlagsCombined | (byte)MessageFlags.IsEncrypted);
+            byteStreamWriter.Write(typeAndFlagsCombined); // byte type + flags
             // no message type hash
 
             var byteStreamReader = new ByteStreamReader(byteStreamWriter, serializer);
@@ -167,7 +208,10 @@ public void MessageReader_TryReadMessage_ReturnsFalse_IfInvalidMessageTypeHash()
 
             // Write test data
             var byteStreamWriter = new ByteStreamWriter(serializer);
-            byteStreamWriter.Write((byte)(MessageFlags.IsEncrypted | MessageFlags.HasRequestId)); // byte flags
+            byte typeAndFlagsCombined = (byte)MessageType.Event;
+            typeAndFlagsCombined = (byte)(typeAndFlagsCombined << 5);
+            typeAndFlagsCombined = (byte)(typeAndFlagsCombined | (byte)MessageFlags.IsEncrypted);
+            byteStreamWriter.Write(typeAndFlagsCombined); // byte type + flags
             byteStreamWriter.Write((ulong)123123); // invalid message type hash
 
             var byteStreamReader = new ByteStreamReader(byteStreamWriter, serializer);
@@ -184,7 +228,10 @@ public void MessageReader_TryReadMessage_ReturnsFalse_IfMissingChannelNumber()
 
             // Write test data
             var byteStreamWriter = new ByteStreamWriter(serializer);
-            byteStreamWriter.Write((byte)(MessageFlags.IsEncrypted | MessageFlags.HasRequestId));
+            byte typeAndFlagsCombined = (byte)MessageType.Event;
+            typeAndFlagsCombined = (byte)(typeAndFlagsCombined << 5);
+            typeAndFlagsCombined = (byte)(typeAndFlagsCombined | (byte)MessageFlags.IsEncrypted);
+            byteStreamWriter.Write(typeAndFlagsCombined); // byte type + flags
             byteStreamWriter.Write((ulong)typeHashMap.GetTypeHash<TestResponse>());
             // missing channel number byte
 
@@ -202,7 +249,10 @@ public void MessageReader_TryReadMessage_ReturnsFalse_IfMissingRequestId()
 
             // Write test data
             var byteStreamWriter = new ByteStreamWriter(serializer);
-            byteStreamWriter.Write((byte)(MessageFlags.IsEncrypted | MessageFlags.HasRequestId));
+            byte typeAndFlagsCombined = (byte)MessageType.Request;
+            typeAndFlagsCombined = (byte)(typeAndFlagsCombined << 5);
+            typeAndFlagsCombined = (byte)(typeAndFlagsCombined | (byte)MessageFlags.IsEncrypted);
+            byteStreamWriter.Write(typeAndFlagsCombined); // byte type + flags
             byteStreamWriter.Write((ulong)typeHashMap.GetTypeHash<TestResponse>());
             byteStreamWriter.Write((byte)123);
             // missing request id short, while HasRequestId flag is set
@@ -221,7 +271,10 @@ public void MessageReader_TryReadMessage_ReadsEvent_WithDebugFlag()
 
             // Write test data
             var byteStreamWriter = new ByteStreamWriter(serializer);
-            byteStreamWriter.Write((byte)(MessageFlags.IsEncrypted | MessageFlags.IsDebug)); // byte flags
+            byte typeAndFlagsCombined = (byte)MessageType.Event;
+            typeAndFlagsCombined = (byte)(typeAndFlagsCombined << 5);
+            typeAndFlagsCombined = (byte)(typeAndFlagsCombined | (byte)(MessageFlags.IsEncrypted | MessageFlags.IsDebug));
+            byteStreamWriter.Write(typeAndFlagsCombined); // byte type + flags
             byteStreamWriter.Write(typeHashMap.GetTypeHash<TestEvent>()); // ulong type hash
             byteStreamWriter.Write((byte)123); // byte Channel number
             byteStreamWriter.Write("test_debug_info_string");
@@ -237,7 +290,6 @@ public void MessageReader_TryReadMessage_ReadsEvent_WithDebugFlag()
             Assert.AreEqual("test", ((TestEvent)messageWrapper.MessageData).Value);
             Assert.AreEqual(123, messageWrapper.Channel);
             Assert.IsTrue(messageWrapper.Flags.HasFlag(MessageFlags.IsEncrypted));
-            Assert.IsFalse(messageWrapper.Flags.HasFlag(MessageFlags.HasRequestId));
             Assert.IsTrue(messageWrapper.Flags.HasFlag(MessageFlags.IsDebug));
             Assert.AreEqual("test_debug_info_string", messageWrapper.DebugInfo);
         }
diff --git a/source/Fenrir.Multiplayer.Tests/Unit/Network/MessageWriterTests.cs b/source/Fenrir.Multiplayer.Tests/Unit/Network/MessageWriterTests.cs
index 485e1c8..7318598 100644
--- a/source/Fenrir.Multiplayer.Tests/Unit/Network/MessageWriterTests.cs
+++ b/source/Fenrir.Multiplayer.Tests/Unit/Network/MessageWriterTests.cs
@@ -7,7 +7,7 @@ namespace Fenrir.Multiplayer.Tests.Unit.LiteNetProtocol
     public class MessageWriterTests
     {
         // Message format: 
-        // 1. [1 byte flags]
+        // 1. [1 byte message type + flags]
         // 2. [8 bytes long message type hash]
         // 3. [1 byte channel number]
         // 4. [2 bytes short requestId] - optional, if flags has HasRequestId
@@ -29,10 +29,14 @@ public void MessageWriter_WriteMessage_WritesEvent()
             // Validate 
             var byteStreamReader = new ByteStreamReader(byteStreamWriter, serializer);
 
-            // Read flags
-            MessageFlags flags = (MessageFlags)byteStreamReader.ReadByte();
+            // Read type + flags
+            byte typeAndFlagsCombined = byteStreamReader.ReadByte();
+            byte typeByte = (byte)(typeAndFlagsCombined >> 5);
+            MessageType type = (MessageType)typeByte;
+            Assert.AreEqual(MessageType.Event, type);
+            byte flagsByte = (byte)(typeAndFlagsCombined & 0b111); 
+            MessageFlags flags = (MessageFlags)flagsByte;
             Assert.IsTrue(flags.HasFlag(MessageFlags.IsEncrypted));
-            Assert.IsFalse(flags.HasFlag(MessageFlags.HasRequestId));
 
             // Read hash
             Assert.AreEqual(typeHashMap.GetTypeHash<TestEvent>(), byteStreamReader.ReadULong()); // [ulong] type hash
@@ -54,21 +58,60 @@ public void MessageWriter_WriteMessage_WritesRequest()
             var messageWriter = new MessageWriter(serializer, typeHashMap, new EventBasedLogger());
 
             var byteStreamWriter = new ByteStreamWriter(serializer);
-            var messageWrapper = MessageWrapper.WrapRequest(new TestRequest() { Value = "test" }, 456, 123, MessageFlags.IsEncrypted | MessageFlags.HasRequestId, MessageDeliveryMethod.ReliableOrdered);
+            var messageWrapper = MessageWrapper.WrapRequest(new TestRequest() { Value = "test" }, 123, MessageFlags.IsEncrypted, MessageDeliveryMethod.ReliableOrdered);
 
             messageWriter.WriteMessage(byteStreamWriter, messageWrapper);
 
             // Validate 
             var byteStreamReader = new ByteStreamReader(byteStreamWriter, serializer);
-
-            // Read flags
-            MessageFlags flags = (MessageFlags)byteStreamReader.ReadByte();
+            
+            // Read type + flags
+            byte typeAndFlagsCombined = byteStreamReader.ReadByte();
+            byte typeByte = (byte)(typeAndFlagsCombined >> 5);
+            MessageType type = (MessageType)typeByte;
+            Assert.AreEqual(MessageType.Request, type);
+            byte flagsByte = (byte)(typeAndFlagsCombined & 0b111); 
+            MessageFlags flags = (MessageFlags)flagsByte;
             Assert.IsTrue(flags.HasFlag(MessageFlags.IsEncrypted));
-            Assert.IsTrue(flags.HasFlag(MessageFlags.HasRequestId));
 
             // Read hash
             Assert.AreEqual(typeHashMap.GetTypeHash<TestRequest>(), byteStreamReader.ReadULong()); // [ulong] type hash
 
+            // Read channel id
+            Assert.AreEqual(123, byteStreamReader.ReadByte());
+
+            // Read data
+            var testRequest = serializer.Deserialize<TestRequest>(byteStreamReader);
+
+            Assert.AreEqual("test", testRequest.Value);
+        }
+
+        [TestMethod]
+        public void MessageWriter_WriteMessage_WritesRequestWithResponse()
+        {
+            var typeHashMap = new TypeHashMap();
+            var serializer = new NetworkSerializer();
+            var messageWriter = new MessageWriter(serializer, typeHashMap, new EventBasedLogger());
+
+            var byteStreamWriter = new ByteStreamWriter(serializer);
+            var messageWrapper = MessageWrapper.WrapRequestWithResponse(new TestRequest() { Value = "test" }, 456, 123, MessageFlags.IsEncrypted, MessageDeliveryMethod.ReliableOrdered);
+
+            messageWriter.WriteMessage(byteStreamWriter, messageWrapper);
+
+            // Validate 
+            var byteStreamReader = new ByteStreamReader(byteStreamWriter, serializer);
+
+            // Read type + flags
+            byte typeAndFlagsCombined = byteStreamReader.ReadByte();
+            byte typeByte = (byte)(typeAndFlagsCombined >> 5);
+            MessageType type = (MessageType)typeByte;
+            Assert.AreEqual(MessageType.RequestWithResponse, type);
+            byte flagsByte = (byte)(typeAndFlagsCombined & 0b111);
+            MessageFlags flags = (MessageFlags)flagsByte;
+            Assert.IsTrue(flags.HasFlag(MessageFlags.IsEncrypted));
+
+            // Read hash
+            Assert.AreEqual(typeHashMap.GetTypeHash<TestRequest>(), byteStreamReader.ReadULong()); // [ulong] type hash
 
             // Read channel id
             Assert.AreEqual(123, byteStreamReader.ReadByte());
@@ -82,7 +125,6 @@ public void MessageWriter_WriteMessage_WritesRequest()
             Assert.AreEqual("test", testRequest.Value);
         }
 
-
         [TestMethod]
         public void MessageWriter_WriteMessage_WritesResponse()
         {
@@ -91,17 +133,21 @@ public void MessageWriter_WriteMessage_WritesResponse()
             var messageWriter = new MessageWriter(serializer, typeHashMap, new EventBasedLogger());
 
             var byteStreamWriter = new ByteStreamWriter(serializer);
-            var messageWrapper = MessageWrapper.WrapResponse(new TestResponse() { Value = "test" }, 456, 123, MessageFlags.IsEncrypted | MessageFlags.HasRequestId, MessageDeliveryMethod.ReliableOrdered);
+            var messageWrapper = MessageWrapper.WrapResponse(new TestResponse() { Value = "test" }, 456, 123, MessageFlags.IsEncrypted, MessageDeliveryMethod.ReliableOrdered);
 
             messageWriter.WriteMessage(byteStreamWriter, messageWrapper);
 
             // Validate 
             var byteStreamReader = new ByteStreamReader(byteStreamWriter, serializer);
 
-            // Read flags
-            MessageFlags flags = (MessageFlags)byteStreamReader.ReadByte();
+            // Read type + flags
+            byte typeAndFlagsCombined = byteStreamReader.ReadByte();
+            byte typeByte = (byte)(typeAndFlagsCombined >> 5);
+            MessageType type = (MessageType)typeByte;
+            Assert.AreEqual(MessageType.Response, type);
+            byte flagsByte = (byte)(typeAndFlagsCombined & 0b111);
+            MessageFlags flags = (MessageFlags)flagsByte;
             Assert.IsTrue(flags.HasFlag(MessageFlags.IsEncrypted));
-            Assert.IsTrue(flags.HasFlag(MessageFlags.HasRequestId));
 
             // Read hash
             Assert.AreEqual(typeHashMap.GetTypeHash<TestResponse>(), byteStreamReader.ReadULong()); // [ulong] type hash
@@ -134,10 +180,14 @@ public void MessageWriter_WriteMessage_WritesDebugInfo_WhenIsDebugFlagSet()
             // Validate 
             var byteStreamReader = new ByteStreamReader(byteStreamWriter, serializer);
 
-            // Read flags
-            MessageFlags flags = (MessageFlags)byteStreamReader.ReadByte();
+            // Read type + flags
+            byte typeAndFlagsCombined = byteStreamReader.ReadByte();
+            byte typeByte = (byte)(typeAndFlagsCombined >> 5);
+            MessageType type = (MessageType)typeByte;
+            Assert.AreEqual(MessageType.Event, type);
+            byte flagsByte = (byte)(typeAndFlagsCombined & 0b111);
+            MessageFlags flags = (MessageFlags)flagsByte;
             Assert.IsTrue(flags.HasFlag(MessageFlags.IsEncrypted));
-            Assert.IsFalse(flags.HasFlag(MessageFlags.HasRequestId));
             Assert.IsTrue(flags.HasFlag(MessageFlags.IsDebug));
 
             // Read hash
diff --git a/source/Fenrir.Multiplayer/Fenrir.Multiplayer.csproj b/source/Fenrir.Multiplayer/Fenrir.Multiplayer.csproj
index a1f9b78..46f38ca 100644
--- a/source/Fenrir.Multiplayer/Fenrir.Multiplayer.csproj
+++ b/source/Fenrir.Multiplayer/Fenrir.Multiplayer.csproj
@@ -5,7 +5,7 @@
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
     <GeneratePackageOnBuild>true</GeneratePackageOnBuild>
     <Description>Fenrir Multiplayer Library</Description>
-    <Version>1.0.19</Version>
+    <Version>1.0.20</Version>
 	<PackageReadmeFile>README.md</PackageReadmeFile>
   </PropertyGroup>
 
diff --git a/source/UnityPackage/Assets/Runtime/LiteNet/LiteNetClientPeer.cs b/source/UnityPackage/Assets/Runtime/LiteNet/LiteNetClientPeer.cs
index 579a1d0..5b71dee 100644
--- a/source/UnityPackage/Assets/Runtime/LiteNet/LiteNetClientPeer.cs
+++ b/source/UnityPackage/Assets/Runtime/LiteNet/LiteNetClientPeer.cs
@@ -66,11 +66,10 @@ public void SendRequest<TRequest>(TRequest request, byte channel = 0, MessageDel
         public void SendRequest<TRequest>(TRequest request, bool encrypted, byte channel = 0, MessageDeliveryMethod deliveryMethod = MessageDeliveryMethod.ReliableOrdered)
             where TRequest : IRequest
         {
-            short requestId = 0; // Requests with no response, do not require a unique id
             MessageFlags flags = encrypted ? MessageFlags.IsEncrypted : MessageFlags.None;
             flags |= GetDebugFlag();
 
-            MessageWrapper messageWrapper = MessageWrapper.WrapRequest(request, requestId, channel, flags, deliveryMethod);
+            MessageWrapper messageWrapper = MessageWrapper.WrapRequest(request, channel, flags, deliveryMethod);
             Send(messageWrapper);
         }
 
@@ -86,7 +85,7 @@ public async Task<TResponse> SendRequest<TRequest, TResponse>(TRequest request,
             short requestId = GetNextRequestId();
 
             MessageDeliveryMethod deliveryMethod = ordered ? MessageDeliveryMethod.ReliableOrdered : MessageDeliveryMethod.ReliableUnordered; // Requests that require a response are always reliable
-            MessageFlags flags = MessageFlags.HasRequestId;
+            MessageFlags flags = MessageFlags.None;
             if (encrypted)
             {
                 flags |= MessageFlags.IsEncrypted;
@@ -97,7 +96,7 @@ public async Task<TResponse> SendRequest<TRequest, TResponse>(TRequest request,
             }
             flags |= GetDebugFlag();
 
-            MessageWrapper messageWrapper = MessageWrapper.WrapRequest(request, requestId, channel, flags, deliveryMethod);
+            MessageWrapper messageWrapper = MessageWrapper.WrapRequestWithResponse(request, requestId, channel, flags, deliveryMethod);
 
             // Add request awaiter to a response map
             Task<MessageWrapper> task = _pendingRequestMap.OnSendRequest(messageWrapper);
diff --git a/source/UnityPackage/Assets/Runtime/LiteNet/LiteNetProtocolListener.cs b/source/UnityPackage/Assets/Runtime/LiteNet/LiteNetProtocolListener.cs
index d0c12f9..071d68a 100644
--- a/source/UnityPackage/Assets/Runtime/LiteNet/LiteNetProtocolListener.cs
+++ b/source/UnityPackage/Assets/Runtime/LiteNet/LiteNetProtocolListener.cs
@@ -414,7 +414,7 @@ private async Task HandleNetworkReceive(NetPeer netPeer, NetPacketReader netPack
             }
 
             // Dispatch message
-            if (messageWrapper.MessageType == MessageType.Request)
+            if (messageWrapper.MessageType == MessageType.Request || messageWrapper.MessageType == MessageType.RequestWithResponse)
             {
                 // Request
                 IRequest request = messageWrapper.MessageData as IRequest;
diff --git a/source/UnityPackage/Assets/Runtime/LiteNet/LiteNetServerPeer.cs b/source/UnityPackage/Assets/Runtime/LiteNet/LiteNetServerPeer.cs
index 52975af..01853d0 100644
--- a/source/UnityPackage/Assets/Runtime/LiteNet/LiteNetServerPeer.cs
+++ b/source/UnityPackage/Assets/Runtime/LiteNet/LiteNetServerPeer.cs
@@ -79,7 +79,7 @@ public void SendResponse<TResponse>(TResponse response, short requestId, bool en
         {
             MessageDeliveryMethod deliveryMethod = ordered ? MessageDeliveryMethod.ReliableOrdered : MessageDeliveryMethod.ReliableUnordered; // Responses are always reliable
 
-            MessageFlags flags = MessageFlags.HasRequestId; // Responses always have request id
+            MessageFlags flags = MessageFlags.None;
             if(encrypted)
             {
                 flags |= MessageFlags.IsEncrypted;
diff --git a/source/UnityPackage/Assets/Runtime/Network/MessageFlags.cs b/source/UnityPackage/Assets/Runtime/Network/MessageFlags.cs
index 3fa2389..e37bfed 100644
--- a/source/UnityPackage/Assets/Runtime/Network/MessageFlags.cs
+++ b/source/UnityPackage/Assets/Runtime/Network/MessageFlags.cs
@@ -6,33 +6,27 @@ namespace Fenrir.Multiplayer
     /// Message Flags
     /// </summary>
     [Flags]
-    enum MessageFlags : byte
+    enum MessageFlags
     {
         /// <summary>
         /// No specific flags
         /// </summary>
         None = 0,
 
-        /// <summary>
-        /// Indicates if message has unique request id.
-        /// This is true for requests that require a response and responses.
-        /// </summary>
-        HasRequestId = 1,
-
         /// <summary>
         /// Indicates if message is encrypted
         /// </summary>
-        IsEncrypted = 2,
+        IsEncrypted = 1,
 
         /// <summary>
         /// Indicates if responses should arrive in order in the selected channel
         /// </summary>
-        IsOrdered = 4,
+        IsOrdered = 2,
 
         /// <summary>
         /// If set to ture, message contains Debug information.
         /// This flag affects netcode performance and should be disabled in production builds.
         /// </summary>
-        IsDebug = 8,
+        IsDebug = 4,
     }
 }
diff --git a/source/UnityPackage/Assets/Runtime/Network/MessageReader.cs b/source/UnityPackage/Assets/Runtime/Network/MessageReader.cs
index 4e8b101..46ae88a 100644
--- a/source/UnityPackage/Assets/Runtime/Network/MessageReader.cs
+++ b/source/UnityPackage/Assets/Runtime/Network/MessageReader.cs
@@ -55,7 +55,7 @@ public bool TryReadMessage(ByteStreamReader byteStreamReader, out MessageWrapper
             // TODO: Encryption
 
             // Message format: 
-            // 1. [1 byte flags]
+            // 1. [1 byte message type + flags]
             // 2. [8 bytes long message type hash]
             // 3. [1 byte channel number]
             // 4. [2 bytes short requestId] - optional, if flags has HasRequestId
@@ -63,14 +63,18 @@ public bool TryReadMessage(ByteStreamReader byteStreamReader, out MessageWrapper
 
             messageWrapper = default;
 
-            // 1. byte Flags
-            if (!byteStreamReader.TryReadByte(out byte flagBytes))
+            // 1. byte Message type + flags
+            if (!byteStreamReader.TryReadByte(out byte typeAndFlagsCombined))
             {
-                _logger.Warning("Malformed message: no flags");
+                _logger.Warning("Malformed message: no message type and flags");
                 return false;
             }
+            byte messageTypeByte = (byte)(typeAndFlagsCombined >> 5);
+            MessageType messageType = (MessageType)messageTypeByte;
+
+            byte messageFlagsByte = (byte)(typeAndFlagsCombined & 0b111); // Clear front 5 bits to make sure conversion to MessageFlags works correctly
+            MessageFlags messageFlags = (MessageFlags)messageFlagsByte;
 
-            MessageFlags messageFlags = (MessageFlags)flagBytes; // Flags enum
 
             // 2. ulong Message type hash
             if (!byteStreamReader.TryReadULong(out ulong messageTypeHash))
@@ -88,7 +92,7 @@ public bool TryReadMessage(ByteStreamReader byteStreamReader, out MessageWrapper
 
             // 4. short request id
             short requestId = 0;
-            if (messageFlags.HasFlag(MessageFlags.HasRequestId))
+            if (messageType == MessageType.RequestWithResponse || messageType == MessageType.Response)
             {
                 if (!byteStreamReader.TryReadShort(out requestId))
                 {
@@ -104,7 +108,7 @@ public bool TryReadMessage(ByteStreamReader byteStreamReader, out MessageWrapper
                 debugInfo = byteStreamReader.ReadString();
             }
 
-            // Find message type
+            // Find message data type
             if (!_typeHashMap.TryGetTypeByHash(messageTypeHash, out Type dataType))
             {
                 if(debugInfo != null)
@@ -135,23 +139,25 @@ public bool TryReadMessage(ByteStreamReader byteStreamReader, out MessageWrapper
                 _byteStreamReaderPool.Return(byteStreamReader);
             }
 
-            // Check data type
-            MessageType messageType;
-            if(messageData is IEvent)
+            // Validate incoming message type
+            if(messageType == MessageType.Request && !(messageData is IRequest))
             {
-                messageType = MessageType.Event;
+                _logger.Warning($"Malformed message: message sent as Request but data type {dataType.Name} is not {nameof(IRequest)}");
+                return false;
             }
-            else if(messageData is IRequest)
+            else if(messageType == MessageType.RequestWithResponse && !(messageData is IRequest))
             {
-                messageType = MessageType.Request;
+                _logger.Warning($"Malformed message: message sent as Request but message data type {dataType.Name} is not {nameof(IRequest)}");
+                return false;
             }
-            else if(messageData is IResponse)
+            else if(messageType == MessageType.Response && !(messageData is IResponse))
             {
-                messageType = MessageType.Response;
+                _logger.Warning($"Malformed message: message sent as Response but message data type {dataType.Name} is not {nameof(IResponse)}");
+                return false;
             }
-            else
+            else if(messageType == MessageType.Event && !(messageData is IEvent))
             {
-                _logger.Warning("Malformed message: unknown message type {0}, must be Event, Request or Response", dataType.Name);
+                _logger.Warning($"Malformed message: message sent as Event but message data type {dataType.Name} is not {nameof(IEvent)}");
                 return false;
             }
 
diff --git a/source/UnityPackage/Assets/Runtime/Network/MessageType.cs b/source/UnityPackage/Assets/Runtime/Network/MessageType.cs
index e17aef9..d460048 100644
--- a/source/UnityPackage/Assets/Runtime/Network/MessageType.cs
+++ b/source/UnityPackage/Assets/Runtime/Network/MessageType.cs
@@ -3,25 +3,34 @@
     /// <summary>
     /// Type of the message
     /// </summary>
-    enum MessageType : byte
+    enum MessageType
     {
         /// <summary>
-        /// Event
-        /// Events are sent from server to client,
-        /// to notify client(s) of a state change
+        /// Raw bytes
+        /// </summary>
+        RawBytes = 0,
+
+        /// <summary>
+        /// Request with no response
         /// </summary>
-        Event,
+        Request = 1,
 
         /// <summary>
-        /// Request
-        /// Request are sent from client to server, and might require a response
+        /// Request that requires a response
         /// </summary>
-        Request,
+        RequestWithResponse = 2,
 
         /// <summary>
         /// Response
         /// Responses are sent back from server to client, as a result of a given request
         /// </summary>
-        Response
+        Response = 3,
+
+        /// <summary>
+        /// Event
+        /// Events are sent from server to client,
+        /// to notify client(s) of a state change
+        /// </summary>
+        Event = 4,
     }
 }
diff --git a/source/UnityPackage/Assets/Runtime/Network/MessageWrapper.cs b/source/UnityPackage/Assets/Runtime/Network/MessageWrapper.cs
index 54cd84c..441c600 100644
--- a/source/UnityPackage/Assets/Runtime/Network/MessageWrapper.cs
+++ b/source/UnityPackage/Assets/Runtime/Network/MessageWrapper.cs
@@ -6,13 +6,13 @@
     internal struct MessageWrapper
     {
         /// <summary>
-        /// Type of the message: Event, Request or Response
+        /// Type of the message: Raw, Event, Request or Response
         /// </summary>
         public MessageType MessageType;
 
         /// <summary>
         /// Message data object. 
-        /// If <see cref="MessageType"/> is <see cref="MessageType.Request"/>, should be <see cref="IRequest"/>.
+        /// If <see cref="MessageType"/> is <see cref="MessageType.Request"/> or <see cref="MessageType.RequestWithResponse"/>, should be <see cref="IRequest"/>.
         /// If <see cref="MessageType"/> is <see cref="MessageType.Response"/>, should be <see cref="IResponse"/>.
         /// If <see cref="MessageType"/> is <see cref="MessageType.Event"/>, should be <see cref="IEvent"/>.
         /// </summary>
@@ -102,14 +102,27 @@ public static MessageWrapper WrapEvent(IEvent data, byte channel, MessageFlags f
         /// Credates message wrapper for a request
         /// </summary>
         /// <param name="data">Request data. <seealso cref="MessageData"/></param>
+        /// <param name="channel">Channel number. <seealso cref="Channel"/></param>
+        /// <param name="flags">Message flags. <see cref="Flags"/></param>
+        /// <param name="deliveryMethod">Delivery method. <seealso cref="DeliveryMethod"/></param>
+        /// <returns>New MessageWrapper that wraps given request</returns>
+        public static MessageWrapper WrapRequest(IRequest data, byte channel, MessageFlags flags, MessageDeliveryMethod deliveryMethod)
+        {
+            return new MessageWrapper(MessageType.Request, data, 0, channel, flags, deliveryMethod, null);
+        }
+
+        /// <summary>
+        /// Credates message wrapper for a request with response
+        /// </summary>
+        /// <param name="data">Request data. <seealso cref="MessageData"/></param>
         /// <param name="requestId">Request id. <seealso cref="RequestId"/></param>
         /// <param name="channel">Channel number. <seealso cref="Channel"/></param>
         /// <param name="flags">Message flags. <see cref="Flags"/></param>
         /// <param name="deliveryMethod">Delivery method. <seealso cref="DeliveryMethod"/></param>
         /// <returns>New MessageWrapper that wraps given request</returns>
-        public static MessageWrapper WrapRequest(IRequest data, short requestId, byte channel, MessageFlags flags, MessageDeliveryMethod deliveryMethod)
+        public static MessageWrapper WrapRequestWithResponse(IRequest data, short requestId, byte channel, MessageFlags flags, MessageDeliveryMethod deliveryMethod)
         {
-            return new MessageWrapper(MessageType.Request, data, requestId, channel, flags, deliveryMethod, null);
+            return new MessageWrapper(MessageType.RequestWithResponse, data, requestId, channel, flags, deliveryMethod, null);
         }
 
         /// <summary>
diff --git a/source/UnityPackage/Assets/Runtime/Network/MessageWriter.cs b/source/UnityPackage/Assets/Runtime/Network/MessageWriter.cs
index a6e6bcb..8507777 100644
--- a/source/UnityPackage/Assets/Runtime/Network/MessageWriter.cs
+++ b/source/UnityPackage/Assets/Runtime/Network/MessageWriter.cs
@@ -44,14 +44,17 @@ public void WriteMessage(ByteStreamWriter byteStreamWriter, MessageWrapper messa
             // TODO: Encryption
 
             // Message format: 
-            // 1. [1 byte flags]
+            // 1. [1 byte message type + flags]
             // 2. [8 bytes long message type hash]
             // 3. [1 byte channel number]
             // 4. [2 bytes short requestId] - optional, if flags has HasRequestId
             // 5. [N bytes serialized message]
 
-            // 1. byte Message flags
-            byteStreamWriter.Write((byte)messageWrapper.Flags);
+            // 1. byte Message type + flags
+            byte typeAndFlagsCombined = (byte)messageWrapper.MessageType;
+            typeAndFlagsCombined = (byte)(typeAndFlagsCombined << 5);
+            typeAndFlagsCombined = (byte)(typeAndFlagsCombined | (byte)messageWrapper.Flags);
+            byteStreamWriter.Write(typeAndFlagsCombined);
 
             // 2. ulong Message type hash
             ulong messageTypeHash = _typeHashMap.GetTypeHash(messageWrapper.MessageData.GetType());
@@ -61,7 +64,7 @@ public void WriteMessage(ByteStreamWriter byteStreamWriter, MessageWrapper messa
             byteStreamWriter.Write(messageWrapper.Channel);
 
             // 4. short Request id
-            if (messageWrapper.Flags.HasFlag(MessageFlags.HasRequestId))
+            if (messageWrapper.MessageType == MessageType.RequestWithResponse || messageWrapper.MessageType == MessageType.Response)
             {
                 byteStreamWriter.Write(messageWrapper.RequestId);
             }
diff --git a/source/UnityPackage/Assets/package.json b/source/UnityPackage/Assets/package.json
index cacb37d..640e1c4 100644
--- a/source/UnityPackage/Assets/package.json
+++ b/source/UnityPackage/Assets/package.json
@@ -3,7 +3,7 @@
     "displayName": "Fenrir Multiplayer",
     "description": "Library for building multiplayer games with using Fenrir Multiplayer Platform",
     "license": "MIT",
-    "version": "1.0.19",
+    "version": "1.0.20",
     "author": {
         "name": "Fenrir",
         "email": "info@fenrirserver.com",