diff --git a/JKMP.Plugin.Multiplayer/Matchmaking/MatchmakingManager.cs b/JKMP.Plugin.Multiplayer/Matchmaking/MatchmakingManager.cs index e424bf5..f30c3a0 100644 --- a/JKMP.Plugin.Multiplayer/Matchmaking/MatchmakingManager.cs +++ b/JKMP.Plugin.Multiplayer/Matchmaking/MatchmakingManager.cs @@ -88,6 +88,11 @@ await Client.Connect(endpoint, { break; } + catch (Exception ex) + { + Logger.Error(ex, "Matchmaking thread raised an unhandled exception"); + // Ignore for now to ensure we don't crash the game + } if (matchmakingCancellationSource.IsCancellationRequested) break; diff --git a/Matchmaking.Client/Networking/Framed.cs b/Matchmaking.Client/Networking/Framed.cs index d5eb60d..02ed09d 100644 --- a/Matchmaking.Client/Networking/Framed.cs +++ b/Matchmaking.Client/Networking/Framed.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Net.Sockets; @@ -30,34 +31,134 @@ public Framed(TStream stream, TCodec sink) if (!Stream.CanRead) return default; - int numBytes; + Tuple msgLengthTuple = await ReadMessageLength(Stream); + if (!msgLengthTuple.Item1) + return default; + + ulong messageLength = msgLengthTuple.Item2; + + byte varIntLength = EncodingUtility.GetVarIntLength(messageLength); + + if (messageLength > int.MaxValue || (int)messageLength > recvBuffer.Length) + throw new InvalidDataException($"Message too large (length = {msgLengthTuple})"); + + if (!await ReadBytes(Stream, (int)messageLength, recvBuffer)) + return default; + + // tfw no span + byte[] bytes = new byte[messageLength + varIntLength]; + + // Write message length to first n bytes + using (var writer = new BinaryWriter(new MemoryStream(bytes, true))) + { + writer.WriteVarInt(messageLength); + } + + Array.Copy(recvBuffer, 0, bytes, varIntLength, (int)messageLength); + + using var memoryStream = new MemoryStream(bytes); + using var reader = new BinaryReader(memoryStream); + try { - numBytes = await Stream.ReadAsync(recvBuffer, 0, recvBuffer.Length, cancellationToken); + return Codec.Decode(reader); } - catch (ObjectDisposedException) // Socket was closed + catch (FormatException ex) { - return default; + throw new FormatException($"Decoding incoming message failed.\nBytes: [{BytesToString(bytes)}]", ex); } - catch (TaskCanceledException) + } + + private async Task> ReadMessageLength(TStream stream) + { + Tuple Success(ulong val) => new(true, val); + Tuple Fail() => new(false, default); + + byte[] discriminatorBytes = new byte[1]; + if (!await ReadBytes(stream, 1, discriminatorBytes)) + return Fail(); + + var varIntLength = EncodingUtility.GetVarIntLength(discriminatorBytes[0]); + + switch (varIntLength) { - return default; + case 1: // byte + return Success(discriminatorBytes[0]); + case 3: // ushort + { + byte[] bytes = new byte[2]; + + if (!await ReadBytes(stream, 2, bytes)) + return Fail(); + + return Success(BitConverter.ToUInt16(bytes, 0)); + } + case 5: // uint + { + byte[] bytes = new byte[4]; + + if (!await ReadBytes(stream, 4, bytes)) + return Fail(); + + return Success(BitConverter.ToUInt32(bytes, 0)); + } + case 9: // ulong + { + byte[] bytes = new byte[8]; + + if (!await ReadBytes(stream, 8, bytes)) + return Fail(); + + return Success(BitConverter.ToUInt64(bytes, 0)); + } + default: throw new NotImplementedException(); } + } - if (numBytes == 0) // EOF/Disconnected - return default; + private async Task ReadBytes(TStream stream, int length, byte[] buffer) + { + int numBytes = 0; - // tfw no span - byte[] bytes = new byte[numBytes]; - Array.Copy(recvBuffer, bytes, numBytes); + while (numBytes < length) + { + int read; - using var memoryStream = new MemoryStream(bytes); - using var reader = new BinaryReader(memoryStream); + try + { + read = await stream.ReadAsync(buffer, numBytes, length - numBytes); + } + catch (ObjectDisposedException) // Socket was closed + { + return false; + } + catch (TaskCanceledException) + { + return false; + } - // todo: support reading multiple messages in a packet - - return Codec.Decode(reader); + if (read == 0) + return false; + + numBytes += read; + } + + return true; + } + + private string BytesToString(byte[] bytes) + { + StringBuilder builder = new(); + + for (int i = 0; i < bytes.Length; ++i) + { + builder.Append("0x" + bytes[i].ToString("X2")); + + if (i < bytes.Length - 1) + builder.Append(", "); + } + + return builder.ToString(); } public async Task Send(TData data) diff --git a/Matchmaking.Client/Networking/MessagesCodec.cs b/Matchmaking.Client/Networking/MessagesCodec.cs index d97602c..bb233bd 100644 --- a/Matchmaking.Client/Networking/MessagesCodec.cs +++ b/Matchmaking.Client/Networking/MessagesCodec.cs @@ -53,10 +53,10 @@ public override Message Decode(BinaryReader reader) Message message = (Message)Activator.CreateInstance(clrType); message.Deserialize(reader); - available = (ulong)(reader.BaseStream.Position - reader.BaseStream.Length); + available = (ulong)(reader.BaseStream.Length - reader.BaseStream.Position); if (available > 0) - throw new FormatException($"Deserialized message did not consume the full length of the message (remaining bytes: {available}"); + throw new FormatException($"Deserialized message did not consume the full length of the message (remaining bytes: {available})"); return message; } diff --git a/Resources/UI/Chat/ChatMessage.xmmp b/Resources/UI/Chat/ChatMessage.xmmp index 55bac1b..7f46ad8 100644 --- a/Resources/UI/Chat/ChatMessage.xmmp +++ b/Resources/UI/Chat/ChatMessage.xmmp @@ -5,8 +5,8 @@ -