From 8763a089a5f2f4edeb2d9cd5c9b56adaced83e50 Mon Sep 17 00:00:00 2001 From: Maxim Liven Date: Fri, 15 Nov 2024 14:38:46 +0300 Subject: [PATCH 01/31] feat: remove all data --- sources/McQuery.Net/Data/Packages/Request.cs | 13 -- .../Data/Packages/Responses/IResponse.cs | 10 - .../Data/Packages/Responses/RawResponse.cs | 16 -- .../Responses/ServerBasicStateResponse.cs | 60 ----- .../Responses/ServerFullStateResponse.cs | 76 ------- .../Packages/Responses/TimeoutResponse.cs | 16 -- .../Data/Packages/Responses/WrongResponse.cs | 17 -- sources/McQuery.Net/Data/Server.cs | 77 ------- .../McQuery.Net/Services/McQueryService.cs | 205 ------------------ .../Services/RequestFormingService.cs | 81 ------- .../Services/ResposeParsingService.cs | 171 --------------- .../Services/SessionIdProviderService.cs | 68 ------ .../Services/UdpSendReceiveService.cs | 74 ------- sources/McQuery.Net/Utills/ByteCounter.cs | 40 ---- 14 files changed, 924 deletions(-) delete mode 100644 sources/McQuery.Net/Data/Packages/Request.cs delete mode 100644 sources/McQuery.Net/Data/Packages/Responses/IResponse.cs delete mode 100644 sources/McQuery.Net/Data/Packages/Responses/RawResponse.cs delete mode 100644 sources/McQuery.Net/Data/Packages/Responses/ServerBasicStateResponse.cs delete mode 100644 sources/McQuery.Net/Data/Packages/Responses/ServerFullStateResponse.cs delete mode 100644 sources/McQuery.Net/Data/Packages/Responses/TimeoutResponse.cs delete mode 100644 sources/McQuery.Net/Data/Packages/Responses/WrongResponse.cs delete mode 100644 sources/McQuery.Net/Data/Server.cs delete mode 100644 sources/McQuery.Net/Services/McQueryService.cs delete mode 100644 sources/McQuery.Net/Services/RequestFormingService.cs delete mode 100644 sources/McQuery.Net/Services/ResposeParsingService.cs delete mode 100644 sources/McQuery.Net/Services/SessionIdProviderService.cs delete mode 100644 sources/McQuery.Net/Services/UdpSendReceiveService.cs delete mode 100644 sources/McQuery.Net/Utills/ByteCounter.cs diff --git a/sources/McQuery.Net/Data/Packages/Request.cs b/sources/McQuery.Net/Data/Packages/Request.cs deleted file mode 100644 index d67f1f9..0000000 --- a/sources/McQuery.Net/Data/Packages/Request.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace MCQueryLib.Data.Packages -{ - public class Request - { - public byte[] RawRequestData { get; private set; } - public byte RequestType => RawRequestData[2]; - - public Request(byte[] rawRequestData) - { - RawRequestData = rawRequestData; - } - } -} diff --git a/sources/McQuery.Net/Data/Packages/Responses/IResponse.cs b/sources/McQuery.Net/Data/Packages/Responses/IResponse.cs deleted file mode 100644 index 25d05fa..0000000 --- a/sources/McQuery.Net/Data/Packages/Responses/IResponse.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System; - -namespace MCQueryLib.Data.Packages.Responses -{ - public interface IResponse - { - public Guid ServerUUID { get; } - public byte[] RawData { get; } - } -} diff --git a/sources/McQuery.Net/Data/Packages/Responses/RawResponse.cs b/sources/McQuery.Net/Data/Packages/Responses/RawResponse.cs deleted file mode 100644 index 6d96733..0000000 --- a/sources/McQuery.Net/Data/Packages/Responses/RawResponse.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; - -namespace MCQueryLib.Data.Packages.Responses -{ - public class RawResponse : IResponse - { - public RawResponse(Guid serverUUID, byte[] rawData) - { - ServerUUID = serverUUID; - RawData = rawData; - } - - public Guid ServerUUID { get; } - public byte[] RawData { get; } - } -} diff --git a/sources/McQuery.Net/Data/Packages/Responses/ServerBasicStateResponse.cs b/sources/McQuery.Net/Data/Packages/Responses/ServerBasicStateResponse.cs deleted file mode 100644 index 620c1b5..0000000 --- a/sources/McQuery.Net/Data/Packages/Responses/ServerBasicStateResponse.cs +++ /dev/null @@ -1,60 +0,0 @@ -using System; - -namespace MCQueryLib.Data.Packages.Responses -{ - /// - /// Represents data which is received from BasicState request - /// - public class ServerBasicStateResponse : IResponse - { - public ServerBasicStateResponse(Guid serverUUID, - SessionId sessionId, - string motd, - string gameType, - string map, - int numPlayers, - int maxPlayers, - short hostPort, - string hostIp, - byte[] rawData) - { - ServerUUID = serverUUID; - SessionId = sessionId; - Motd = motd; - GameType = gameType; - Map = map; - NumPlayers = numPlayers; - MaxPlayers = maxPlayers; - HostPort = hostPort; - HostIp = hostIp; - RawData = rawData; - } - - public SessionId SessionId { get; } - - public string Motd { get; } - public string GameType { get; } - public string Map { get; } - public int NumPlayers { get; } - public int MaxPlayers { get; } - public short HostPort { get; } - public string HostIp { get; } - - public Guid ServerUUID { get; } - public byte[] RawData { get; } - - public override string ToString() - { - return "BasicStatus\n" + - $"| {nameof(ServerUUID)}: {ServerUUID}\n" + - $"| {nameof(SessionId)}: {SessionId.GetString()}\n" + - $"| {nameof(Motd)}: {Motd}\n" + - $"| {nameof(GameType)}: {GameType}\n" + - $"| {nameof(Map)}: {Map}\n" + - $"| {nameof(NumPlayers)}: {NumPlayers}\n" + - $"| {nameof(MaxPlayers)}: {MaxPlayers}\n" + - $"| {nameof(HostPort)}: {HostPort}\n" + - $"| {nameof(HostIp)}: {HostIp}"; - } - } -} \ No newline at end of file diff --git a/sources/McQuery.Net/Data/Packages/Responses/ServerFullStateResponse.cs b/sources/McQuery.Net/Data/Packages/Responses/ServerFullStateResponse.cs deleted file mode 100644 index efff6d0..0000000 --- a/sources/McQuery.Net/Data/Packages/Responses/ServerFullStateResponse.cs +++ /dev/null @@ -1,76 +0,0 @@ -using System; - -namespace MCQueryLib.Data.Packages.Responses -{ - /// - /// Represents data which is received from FullState request - /// - public class ServerFullStateResponse : IResponse - { - public ServerFullStateResponse(Guid serverUUID, - SessionId sessionId, - string motd, - string gameType, - string gameId, - string version, - string plugins, - string map, - int numPlayers, - int maxPlayers, - string[] playerList, - int hostPort, - string hostIp, - byte[] rawData) - { - ServerUUID = serverUUID; - SessionId = sessionId; - Motd = motd; - GameType = gameType; - GameId = gameId; - Version = version; - Plugins = plugins; - Map = map; - NumPlayers = numPlayers; - MaxPlayers = maxPlayers; - PlayerList = playerList; - HostPort = hostPort; - HostIp = hostIp; - RawData = rawData; - } - - public SessionId SessionId { get; } - - public string Motd { get; } - public string GameType { get; } - public string GameId { get; } - public string Version { get; } - public string Plugins { get; } - public string Map { get; } - public int NumPlayers { get; } - public int MaxPlayers { get; } - public string[] PlayerList { get; } - public int HostPort { get; } - public string HostIp { get; } - - public Guid ServerUUID { get; } - public byte[] RawData { get; } - - public override string ToString() - { - return "FullStatus\n" + - $"| {nameof(ServerUUID)}: {ServerUUID}\n" + - $"| {nameof(SessionId)}: {SessionId.GetString()}\n" + - $"| {nameof(Motd)}: {Motd}\n" + - $"| {nameof(GameType)}: {GameType}\n" + - $"| {nameof(GameId)}: {GameId}\n" + - $"| {nameof(Version)}: {Version}\n" + - $"| {nameof(Plugins)}: {Plugins}\n" + - $"| {nameof(Map)}: {Map}\n" + - $"| {nameof(NumPlayers)}: {NumPlayers}\n" + - $"| {nameof(MaxPlayers)}: {MaxPlayers}\n" + - $"| {nameof(PlayerList)}: [{string.Join(", ", PlayerList)}]\n" + - $"| {nameof(HostPort)}: {HostPort}\n" + - $"| {nameof(HostIp)}: {HostIp}"; - } - } -} \ No newline at end of file diff --git a/sources/McQuery.Net/Data/Packages/Responses/TimeoutResponse.cs b/sources/McQuery.Net/Data/Packages/Responses/TimeoutResponse.cs deleted file mode 100644 index a2c6b1a..0000000 --- a/sources/McQuery.Net/Data/Packages/Responses/TimeoutResponse.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; - -namespace MCQueryLib.Data.Packages.Responses -{ - public class TimeoutResponse : IResponse - { - public TimeoutResponse(Guid serverUUID) - { - ServerUUID = serverUUID; - } - - public byte[] RawData => throw new NotSupportedException(); - public Guid ServerUUID { get; } - public string Message => "Request is timed out"; - } -} diff --git a/sources/McQuery.Net/Data/Packages/Responses/WrongResponse.cs b/sources/McQuery.Net/Data/Packages/Responses/WrongResponse.cs deleted file mode 100644 index dfef392..0000000 --- a/sources/McQuery.Net/Data/Packages/Responses/WrongResponse.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; - -namespace MCQueryLib.Data.Packages.Responses -{ - public class WrongResponse : IResponse - { - public WrongResponse(Guid serverUUID, byte[] rawData) - { - ServerUUID = serverUUID; - RawData = rawData; - } - - public byte[] RawData { get; } - public Guid ServerUUID { get; } - public string Message => "This response package can't be parsed"; - } -} diff --git a/sources/McQuery.Net/Data/Server.cs b/sources/McQuery.Net/Data/Server.cs deleted file mode 100644 index d43a725..0000000 --- a/sources/McQuery.Net/Data/Server.cs +++ /dev/null @@ -1,77 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Net; -using System.Net.Sockets; -using System.Threading; - -namespace MCQueryLib.Data -{ - // todo: add cancellation token support - public class Server : IDisposable - { - public Server(SessionId sessionId, IPAddress host, int port) - { - UUID = Guid.NewGuid(); - SessionId = sessionId; - Host = host; - Port = port; - ChallengeToken = new(); - UdpClient = new UdpClient(Host.ToString(), Port); - UdpClientSemaphoreSlim = new SemaphoreSlim(0, 1); - UdpClientSemaphoreSlim.Release(); - } - - public Guid UUID { get; } - public SessionId SessionId { get; } - public IPAddress Host { get; } - public int Port { get; } - public ChallengeToken ChallengeToken { get; } - public UdpClient UdpClient { get; private set; } - public SemaphoreSlim UdpClientSemaphoreSlim { get; } - - public async void InvalidateSocket() - { - await UdpClientSemaphoreSlim.WaitAsync(); - UdpClient.Dispose(); - UdpClient = new UdpClient(Host.ToString(), Port); - UdpClientSemaphoreSlim.Release(); - } - - private bool disposed = false; - public void Dispose() - { - Dispose(disposing: true); - GC.SuppressFinalize(this); - } - - public void Dispose(bool disposing) - { - if (!this.disposed) - { - if (disposing) - { - UdpClient.Dispose(); - UdpClientSemaphoreSlim.Dispose(); - } - - disposed = true; - } - } - - ~Server() - { - Dispose(disposing: true); - } - - public override bool Equals(object? obj) - { - return obj is Server server && - EqualityComparer.Default.Equals(UUID, server.UUID); - } - - public override int GetHashCode() - { - return UUID.GetHashCode(); - } - } -} diff --git a/sources/McQuery.Net/Services/McQueryService.cs b/sources/McQuery.Net/Services/McQueryService.cs deleted file mode 100644 index 07d96d1..0000000 --- a/sources/McQuery.Net/Services/McQueryService.cs +++ /dev/null @@ -1,205 +0,0 @@ -using MCQueryLib.Data; -using MCQueryLib.Data.Packages.Responses; -using System; -using System.Collections.Generic; -using System.Net; -using System.Threading.Tasks; - -namespace MCQueryLib.Services -{ - public class McQueryService : IDisposable - { - - public McQueryService(Random random, - uint maxTriesBeforeSocketInvalidate, - int receiveAwaitInterval, - int retryAwayitShortInteval, - int retryAwaitLongInterval) - { - sessionIdProviderService = new SessionIdProviderService(random); - ServersTimeoutCounters = new(); - udpService = new UdpSendReceiveService(receiveAwaitInterval); - - MaxTriesBeforeSocketInvalidate = maxTriesBeforeSocketInvalidate; - RetryAwaitShortInterval = retryAwayitShortInteval; - RetryAwaitLongInterval = retryAwaitLongInterval; - } - - public McQueryService(uint maxTriesBeforeSocketInvalidate, - int receiveAwaitInterval, - int retryAwayitShortInteval, - int retryAwaitLongInterval) - : this(new Random(), maxTriesBeforeSocketInvalidate, receiveAwaitInterval, retryAwayitShortInteval, retryAwaitLongInterval) - { - } - - private readonly SessionIdProviderService sessionIdProviderService; - private readonly UdpSendReceiveService udpService; - private Dictionary ServersTimeoutCounters { get; set; } - public uint MaxTriesBeforeSocketInvalidate { get; set; } - public int RetryAwaitShortInterval { get; set; } - public int RetryAwaitLongInterval { get; set; } - - public Server RegistrateServer(IPEndPoint serverEndPoint) - { - SessionId sessionId = sessionIdProviderService.GenerateRandomId(); - Server server = new(sessionId, serverEndPoint.Address, serverEndPoint.Port); - ServersTimeoutCounters.Add(server, 0); - return server; - } - - public void DisposeServer(Server server) - { - ServersTimeoutCounters.Remove(server); - server.Dispose(); - } - - private void ResetTimeoutCounter(Server server) - { - ServersTimeoutCounters[server] = 0; - } - - private async Task InvalidateChallengeToken(Server server) - { - var request = RequestFormingService.HandshakeRequestPackage(server.SessionId); - IResponse response; - - while (true) - { - response = await udpService.SendReceive(server, request); - - if (response is TimeoutResponse) - { - if (ServersTimeoutCounters[server] > MaxTriesBeforeSocketInvalidate) - { - var delayTask = Task.Delay(RetryAwaitLongInterval); - - server.InvalidateSocket(); - ResetTimeoutCounter(server); - - await delayTask; - continue; - } - - ServersTimeoutCounters[server]++; - await Task.Delay(RetryAwaitShortInterval); - continue; - } - - break; - } - - byte[] challengeToken = ResposeParsingService.ParseHandshake((RawResponse)response); - - server.ChallengeToken.UpdateToken(challengeToken); - } - - public async Task GetBasicStatusCommon(Server server) => await GetBasicStatus(server); - public async Task GetBasicStatus(Server server) - { - if (!server.ChallengeToken.IsFine) - await InvalidateChallengeToken(server); - - IResponse response; - - while (true) - { - var request = RequestFormingService.GetBasicStatusRequestPackage(server.SessionId, server.ChallengeToken); - response = await udpService.SendReceive(server, request); - - if (response is TimeoutResponse) - { - if (ServersTimeoutCounters[server] > MaxTriesBeforeSocketInvalidate) - { - var delayTask = Task.Delay(RetryAwaitLongInterval); - - server.InvalidateSocket(); - var invalidateTask = InvalidateChallengeToken(server); - ResetTimeoutCounter(server); - - Task.WaitAll(new Task[] { delayTask, invalidateTask }); - continue; - } - - ServersTimeoutCounters[server]++; - await Task.Delay(RetryAwaitShortInterval); - continue; - } - - break; - } - - var basicStateResponse = ResposeParsingService.ParseBasicState((RawResponse)response); - return basicStateResponse; - } - - public async Task GetFullStatusCommon(Server server) => await GetFullStatus(server); - public async Task GetFullStatus(Server server) - { - if (!server.ChallengeToken.IsFine) - await InvalidateChallengeToken(server); - - IResponse response; - - while (true) - { - var request = RequestFormingService.GetFullStatusRequestPackage(server.SessionId, server.ChallengeToken); - response = await udpService.SendReceive(server, request); - - if (response is TimeoutResponse) - { - if (ServersTimeoutCounters[server] > MaxTriesBeforeSocketInvalidate) - { - var delayTask = Task.Delay(RetryAwaitLongInterval); - - server.InvalidateSocket(); - var invalidateTask = InvalidateChallengeToken(server); - ResetTimeoutCounter(server); - - Task.WaitAll(new Task[] { delayTask, invalidateTask }); - continue; - } - - ServersTimeoutCounters[server]++; - await Task.Delay(RetryAwaitShortInterval); - continue; - } - - break; - } - - var fullStateResponse = ResposeParsingService.ParseFullState((RawResponse)response); - return fullStateResponse; - } - - private bool disposed = false; - public void Dispose() - { - Dispose(disposing: true); - GC.SuppressFinalize(this); - } - - public void Dispose(bool disposing) - { - if (!this.disposed) - { - if (disposing) - { - foreach (var record in ServersTimeoutCounters) - { - record.Key.Dispose(); - } - } - - ServersTimeoutCounters.Clear(); - - disposed = true; - } - } - - ~McQueryService() - { - Dispose(disposing: true); - } - } -} diff --git a/sources/McQuery.Net/Services/RequestFormingService.cs b/sources/McQuery.Net/Services/RequestFormingService.cs deleted file mode 100644 index 56504f5..0000000 --- a/sources/McQuery.Net/Services/RequestFormingService.cs +++ /dev/null @@ -1,81 +0,0 @@ -#nullable enable -using MCQueryLib.Data; -using MCQueryLib.Data.Packages; -using System; -using System.Collections.Generic; -using System.Runtime.Serialization; - -namespace MCQueryLib.Services -{ - /// - /// This class builds Minecraft Query Packages for requests - /// Wiki: https://wiki.vg/Query - /// - public static class RequestFormingService - { - private static readonly byte[] MagicConst = { 0xfe, 0xfd }; - private static readonly byte[] ChallengeRequestConst = { 0x09 }; - private static readonly byte[] StatusRequestConst = { 0x00 }; - - public static Request HandshakeRequestPackage(SessionId sessionId) - { - var data = new List(224); - data.AddRange(MagicConst); - data.AddRange(ChallengeRequestConst); - sessionId.WriteTo(data); - - var request = new Request(data.ToArray()); - return request; - } - - public static Request GetBasicStatusRequestPackage(SessionId sessionId, ChallengeToken challengeToken) - { - if (challengeToken == null) - { - throw new ChallengeTokenIsNullException(); - } - - var data = new List(416); - data.AddRange(MagicConst); - data.AddRange(StatusRequestConst); - sessionId.WriteTo(data); - challengeToken.WriteTo(data); - - var request = new Request(data.ToArray()); - return request; - } - - public static Request GetFullStatusRequestPackage(SessionId sessionId, ChallengeToken challengeToken) - { - if (challengeToken == null) - { - throw new ChallengeTokenIsNullException(); - } - - var data = new List(544); - data.AddRange(MagicConst); - data.AddRange(StatusRequestConst); - sessionId.WriteTo(data); - challengeToken.WriteTo(data); - data.AddRange(new byte[] { 0x00, 0x00, 0x00, 0x00 }); // Padding - - var request = new Request(data.ToArray()); - return request; - } - } - - public class ChallengeTokenIsNullException : Exception - { - public ChallengeTokenIsNullException() - { - } - - public ChallengeTokenIsNullException(string? message) : base(message) - { - } - - public ChallengeTokenIsNullException(string? message, Exception? innerException) : base(message, innerException) - { - } - } -} diff --git a/sources/McQuery.Net/Services/ResposeParsingService.cs b/sources/McQuery.Net/Services/ResposeParsingService.cs deleted file mode 100644 index 78401f0..0000000 --- a/sources/McQuery.Net/Services/ResposeParsingService.cs +++ /dev/null @@ -1,171 +0,0 @@ -#nullable enable -using MCQueryLib.Data; -using MCQueryLib.Data.Packages.Responses; -using System; -using System.Buffers; -using System.Collections.Generic; -using System.Linq; -using System.Runtime.Serialization; -using System.Text; - -namespace MCQueryLib.Services -{ - /// - /// This class parses Minecraft Query response packages for getting data from it - /// Wiki: https://wiki.vg/Query - /// - public static class ResposeParsingService - { - public static byte ParseType(byte[] data) - { - return data[0]; - } - - public static SessionId ParseSessionId(ref SequenceReader reader) - { - if (reader.UnreadSequence.Length < 4) throw new IncorrectPackageDataException(reader.Sequence.ToArray()); - var sessionIdBytes = new byte[4]; - Span sessionIdSpan = new(sessionIdBytes); - reader.TryCopyTo(sessionIdSpan); - reader.Advance(4); - return new SessionId(sessionIdSpan.ToArray()); - } - - /// - /// Parses response package and returns ChallengeToken - /// - /// RawResponce package - /// byte[] array which contains ChallengeToken as big-endian - public static byte[] ParseHandshake(RawResponse rawResponse) - { - var data = (byte[])rawResponse.RawData.Clone(); - - if (data.Length < 5) throw new IncorrectPackageDataException(data); - var response = BitConverter.GetBytes(int.Parse(Encoding.ASCII.GetString(data, 5, rawResponse.RawData.Length - 6))); - if (BitConverter.IsLittleEndian) - { - response = response.Reverse().ToArray(); - } - - return response; - } - - public static ServerBasicStateResponse ParseBasicState(RawResponse rawResponse) - { - if (rawResponse.RawData.Length <= 5) - throw new IncorrectPackageDataException(rawResponse.RawData); - - SequenceReader reader = new(new ReadOnlySequence(rawResponse.RawData)); - reader.Advance(1); // Skip Type - - var sessionId = ParseSessionId(ref reader); - - var motd = ReadString(ref reader); - var gameType = ReadString(ref reader); - var map = ReadString(ref reader); - var numPlayers = int.Parse(ReadString(ref reader)); - var maxPlayers = int.Parse(ReadString(ref reader)); - - if (!reader.TryReadLittleEndian(out short port)) - throw new IncorrectPackageDataException(rawResponse.RawData); - - var hostIp = ReadString(ref reader); - - ServerBasicStateResponse serverInfo = new( - serverUUID: rawResponse.ServerUUID, - sessionId: sessionId, - motd: motd, - gameType: gameType, - map: map, - numPlayers: numPlayers, - maxPlayers: maxPlayers, - hostPort: port, - hostIp: hostIp, - rawData: (byte[])rawResponse.RawData.Clone() - ); - - return serverInfo; - } - - private static readonly byte[] constant1 = new byte[] { 0x73, 0x70, 0x6C, 0x69, 0x74, 0x6E, 0x75, 0x6D, 0x00, 0x80, 0x00 }; - private static readonly byte[] constant2 = new byte[] { 0x01, 0x70, 0x6C, 0x61, 0x79, 0x65, 0x72, 0x5F, 0x00, 0x00 }; - public static ServerFullStateResponse ParseFullState(RawResponse rawResponse) - { - if (rawResponse.RawData.Length <= 5) - throw new IncorrectPackageDataException(rawResponse.RawData); - - var reader = new SequenceReader(new ReadOnlySequence(rawResponse.RawData)); - reader.Advance(1); // Skip Type - - var sessionId = ParseSessionId(ref reader); - - if (!reader.IsNext(constant1, advancePast: true)) - throw new IncorrectPackageDataException(rawResponse.RawData); - - var statusKeyValues = new Dictionary(); - while (!reader.IsNext(0, advancePast: true)) - { - var key = ReadString(ref reader); - var value = ReadString(ref reader); - statusKeyValues.Add(key, value); - } - - if (!reader.IsNext(constant2, advancePast: true)) // Padding: 10 bytes constant - throw new IncorrectPackageDataException(rawResponse.RawData); - - var players = new List(); - while (!reader.IsNext(0, advancePast: true)) - { - players.Add(ReadString(ref reader)); - } - - ServerFullStateResponse fullState = new - ( - serverUUID: rawResponse.ServerUUID, - sessionId: sessionId, - motd: statusKeyValues["hostname"], - gameType: statusKeyValues["gametype"], - gameId: statusKeyValues["game_id"], - version: statusKeyValues["version"], - plugins: statusKeyValues["plugins"], - map: statusKeyValues["map"], - numPlayers: int.Parse(statusKeyValues["numplayers"]), - maxPlayers: int.Parse(statusKeyValues["maxplayers"]), - playerList: players.ToArray(), - hostIp: statusKeyValues["hostip"], - hostPort: int.Parse(statusKeyValues["hostport"]), - rawData: (byte[])rawResponse.RawData.Clone() - ); - - return fullState; - } - - private static string ReadString(ref SequenceReader reader) - { - if (!reader.TryReadTo(out ReadOnlySequence bytes, delimiter: 0, advancePastDelimiter: true)) - throw new IncorrectPackageDataException("Zero byte not found", reader.Sequence.ToArray()); - - return Encoding.ASCII.GetString(bytes); // а точно ASCII? Может, Utf8? - } - } - - public class IncorrectPackageDataException : Exception - { - public byte[] data { get; } - - public IncorrectPackageDataException(byte[] data) - { - this.data = data; - } - - public IncorrectPackageDataException(string? message, byte[] data) : base(message) - { - this.data = data; - } - - public IncorrectPackageDataException(string? message, Exception? innerException, byte[] data) : base(message, innerException) - { - this.data = data; - } - } -} diff --git a/sources/McQuery.Net/Services/SessionIdProviderService.cs b/sources/McQuery.Net/Services/SessionIdProviderService.cs deleted file mode 100644 index b5cc89f..0000000 --- a/sources/McQuery.Net/Services/SessionIdProviderService.cs +++ /dev/null @@ -1,68 +0,0 @@ -using MCQueryLib.Data; -using MCQueryLib.Utills; -using System; -using System.Collections.Generic; - -namespace MCQueryLib.Services -{ - public class SessionIdProviderService - { - public SessionIdProviderService(Random random) - { - this.random = random; - ReservedIds = new(); - IdCounter = new(); - } - - - private readonly List ReservedIds; - - Random random; - public SessionId GenerateRandomId() - { - byte[] sessionIdData = new byte[4]; - SessionId sessionId; - - do - { - random.NextBytes(sessionIdData); - for (int i = 0; i < sessionIdData.Length; ++i) - { - sessionIdData[i] &= 0x0F; - } - - sessionId = new(sessionIdData); - } - while (IsIdReserved(sessionId)); - - ReserveId(sessionId); - return sessionId; - } - - - private readonly ByteCounter IdCounter = new ByteCounter(); - - public SessionId GetUinqueId() - { - byte[] sessionIdData = new byte[4]; - if (!IdCounter.GetNext(sessionIdData)) - { - // find released sessionIds - } - - SessionId sessionId = new(sessionIdData); - ReserveId(sessionId); - return sessionId; - } - - private void ReserveId(SessionId sessionId) - { - ReservedIds.Add(sessionId); - } - - public bool IsIdReserved(SessionId sessionId) - { - return ReservedIds.IndexOf(sessionId) != -1; - } - } -} diff --git a/sources/McQuery.Net/Services/UdpSendReceiveService.cs b/sources/McQuery.Net/Services/UdpSendReceiveService.cs deleted file mode 100644 index bbed21e..0000000 --- a/sources/McQuery.Net/Services/UdpSendReceiveService.cs +++ /dev/null @@ -1,74 +0,0 @@ -using MCQueryLib.Data; -using MCQueryLib.Data.Packages; -using MCQueryLib.Data.Packages.Responses; -using System; -using System.Net; -using System.Net.Sockets; -using System.Threading.Tasks; - -namespace MCQueryLib.Services -{ - - // todo: add resend N times before returning TimeoutResponce - // todo: add cancellation token support - public class UdpSendReceiveService - { - public UdpSendReceiveService(int receiveAwaitInterval) - { - ReceiveAwaitInterval = receiveAwaitInterval; - } - - public int ReceiveAwaitInterval { get; set; } - - public async Task SendReceive(Server server, Request request) - { - UdpClient client = server.UdpClient; - - IPEndPoint? ipEndPoint = null; - byte[]? response = null; - - await server.UdpClientSemaphoreSlim.WaitAsync(); - await server.UdpClient.SendAsync(request.RawRequestData, request.RawRequestData.Length); - IAsyncResult responseToken; - - try - { - responseToken = server.UdpClient.BeginReceive(null, null); - } - catch (SocketException) - { - server.UdpClientSemaphoreSlim.Release(); - return new TimeoutResponse(server.UUID); - } - - responseToken.AsyncWaitHandle.WaitOne(TimeSpan.FromMilliseconds(ReceiveAwaitInterval)); - if (responseToken.IsCompleted) - { - try - { - response = server.UdpClient.EndReceive(responseToken, ref ipEndPoint); - } - - catch (Exception) - { - server.UdpClientSemaphoreSlim.Release(); - return new TimeoutResponse(server.UUID); - } - } - else - { - server.UdpClientSemaphoreSlim.Release(); - return new TimeoutResponse(server.UUID); - } - - if (response == null) - { - server.UdpClientSemaphoreSlim.Release(); - return new TimeoutResponse(server.UUID); - } - - server.UdpClientSemaphoreSlim.Release(); - return new RawResponse(server.UUID, response); - } - } -} diff --git a/sources/McQuery.Net/Utills/ByteCounter.cs b/sources/McQuery.Net/Utills/ByteCounter.cs deleted file mode 100644 index 44d0544..0000000 --- a/sources/McQuery.Net/Utills/ByteCounter.cs +++ /dev/null @@ -1,40 +0,0 @@ -namespace MCQueryLib.Utills -{ - class ByteCounter - { - private byte[] _countUnits; - - public ByteCounter() - { - _countUnits = new byte[4]; - Reset(); - } - - public bool GetNext(byte[] receiver) - { - for (int i = 0; i < _countUnits.Length; ++i) - { - if (_countUnits[i] < 0x0F) - { - _countUnits[i]++; - _countUnits.CopyTo(receiver, 0); - return true; - } - else - { - _countUnits[i] = 0x00; - } - } - - return false; - } - - public void Reset() - { - for (int i = 0; i < _countUnits.Length; ++i) - { - _countUnits[i] = 0; - } - } - } -} From c9c73fc7bc3c689d2c21b51134c48f4dd73c4187 Mon Sep 17 00:00:00 2001 From: Maxim Liven Date: Fri, 15 Nov 2024 14:43:55 +0300 Subject: [PATCH 02/31] feat: add new structures --- Directory.Packages.props | 7 +- sources/McQuery.Net/Abstract/IExpoirable.cs | 6 ++ sources/McQuery.Net/Data/ChallengeToken.cs | 81 +++++++------------ .../McQuery.Net/Data/Responses/BasicState.cs | 25 ++++++ .../McQuery.Net/Data/Responses/FullState.cs | 33 ++++++++ sources/McQuery.Net/Data/SessionId.cs | 75 +++++++++-------- .../Exceptions/AlreadyExpiredException.cs | 16 ++++ sources/McQuery.Net/GlobalUsings.cs | 3 + sources/McQuery.Net/McQuery.Net.csproj | 10 ++- 9 files changed, 162 insertions(+), 94 deletions(-) create mode 100644 sources/McQuery.Net/Abstract/IExpoirable.cs create mode 100644 sources/McQuery.Net/Data/Responses/BasicState.cs create mode 100644 sources/McQuery.Net/Data/Responses/FullState.cs create mode 100644 sources/McQuery.Net/Exceptions/AlreadyExpiredException.cs create mode 100644 sources/McQuery.Net/GlobalUsings.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index 8dce38b..bfee0ac 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,7 +1,8 @@  - true - - + + + + \ No newline at end of file diff --git a/sources/McQuery.Net/Abstract/IExpoirable.cs b/sources/McQuery.Net/Abstract/IExpoirable.cs new file mode 100644 index 0000000..8211b08 --- /dev/null +++ b/sources/McQuery.Net/Abstract/IExpoirable.cs @@ -0,0 +1,6 @@ +namespace McQuery.Net.Abstract; + +internal interface IExpirable +{ + public bool IsExpired { get; } +} \ No newline at end of file diff --git a/sources/McQuery.Net/Data/ChallengeToken.cs b/sources/McQuery.Net/Data/ChallengeToken.cs index ae85551..ca06f7a 100644 --- a/sources/McQuery.Net/Data/ChallengeToken.cs +++ b/sources/McQuery.Net/Data/ChallengeToken.cs @@ -1,53 +1,34 @@ -using System; -using System.Collections.Generic; +using McQuery.Net.Abstract; +using McQuery.Net.Exceptions; -namespace MCQueryLib.Data -{ - public class ChallengeToken - { - private byte[]? _challengeToken; - - private const int alivePeriod = 30000; // Milliseconds before revoking - - private DateTime revokeDateTime; - public bool IsFine => _challengeToken != null && DateTime.Now < revokeDateTime; - - public ChallengeToken() - { - _challengeToken = null; - } - - public ChallengeToken(byte[] challengeToken) - { - UpdateToken(challengeToken); - } - - public void UpdateToken(byte[] challengeToken) - { - _challengeToken = (byte[])challengeToken.Clone(); - revokeDateTime = DateTime.Now.AddMilliseconds(alivePeriod); - } +namespace McQuery.Net.Data; - public string GetString() - { - ArgumentNullException.ThrowIfNull(_challengeToken); - return BitConverter.ToString(_challengeToken); - } - - public byte[] GetBytes() - { - ArgumentNullException.ThrowIfNull(_challengeToken); - - byte[] challengeTokenSnapshot = new byte[4]; - Buffer.BlockCopy(_challengeToken, 0, challengeTokenSnapshot, 0, 4); - return challengeTokenSnapshot; - } - - public void WriteTo(List list) - { - ArgumentNullException.ThrowIfNull(_challengeToken); - - list.AddRange(_challengeToken); - } - } +internal record ChallengeToken : IExpirable +{ + private const int AlivePeriod = 29; + private readonly DateTime _expiresAt = DateTime.UtcNow.AddSeconds(AlivePeriod); + + /// + /// .ctor. + /// + /// Bytes that represents challenge token. + /// + /// Number of bytes is incorrect. + /// + public ChallengeToken(byte[] data) + { + if (data.Length != 4) + throw new ArgumentOutOfRangeException(nameof(data), data, "Challenge token must have 4 bytes"); + + Data = data; + } + + private byte[] Data { get; } + public bool IsExpired => DateTime.UtcNow >= _expiresAt; + + public static implicit operator byte[](ChallengeToken token) + { + AlreadyExpiredException.ThrowIfExpired(token); + return [..token.Data]; + } } diff --git a/sources/McQuery.Net/Data/Responses/BasicState.cs b/sources/McQuery.Net/Data/Responses/BasicState.cs new file mode 100644 index 0000000..1189bf2 --- /dev/null +++ b/sources/McQuery.Net/Data/Responses/BasicState.cs @@ -0,0 +1,25 @@ +namespace McQuery.Net.Data.Responses; + +/// +/// Represents data which is received from BasicState request. +/// +/// +/// . +/// Message of the day. +/// Type of the game. +/// Name of a map. +/// Current number of players. +/// Maximum number of players what is allowed to enter. +/// Port to connect. +/// Ip to connect. +[PublicAPI] +public record BasicState( + SessionId SessionId, + string Motd, + string GameType, + string Map, + int NumPlayers, + int MaxPlayers, + int HostPort, + string HostIp +); diff --git a/sources/McQuery.Net/Data/Responses/FullState.cs b/sources/McQuery.Net/Data/Responses/FullState.cs new file mode 100644 index 0000000..741c0e3 --- /dev/null +++ b/sources/McQuery.Net/Data/Responses/FullState.cs @@ -0,0 +1,33 @@ +namespace McQuery.Net.Data.Responses; + +/// +/// Represents data which is received from FullState request. +/// +/// +/// . +/// Message of the day. +/// Type of the game. +/// Identifier of a game. Constant value: MINECRAFT. +/// Game version number. +/// List of plugins as a string. +/// Name of a map. +/// Current number of players. +/// Maximum number of players what is allowed to enter. +/// List of players' nicknames. +/// Port to connect. +/// Ip to connect. +[PublicAPI] +public record FullState( + SessionId SessionId, + string Motd, + string GameType, + string GameId, + string Version, + string Plugins, + string Map, + int NumPlayers, + int MaxPlayers, + string[] PlayerList, + int HostPort, + string HostIp +); diff --git a/sources/McQuery.Net/Data/SessionId.cs b/sources/McQuery.Net/Data/SessionId.cs index 9c5bf24..76fb5ea 100644 --- a/sources/McQuery.Net/Data/SessionId.cs +++ b/sources/McQuery.Net/Data/SessionId.cs @@ -1,47 +1,46 @@ -using System; -using System.Collections.Generic; -using System.Linq; +namespace McQuery.Net.Data; -namespace MCQueryLib.Data +/// +/// Represents Session Identifier. +/// +[PublicAPI] +public class SessionId { - /// - /// This class represents SessionId filed into packages. - /// It provides api for create random SessionId or parse it from byte[] - /// - public class SessionId - { - private readonly byte[] _sessionId; + private byte[] Data { get; } - public SessionId(byte[] sessionId) - { - _sessionId = sessionId; - } + /// + /// .ctor. + /// + /// Bytes that represents session identifier. + /// + /// Number of bytes is incorrect. + /// + public SessionId(byte[] data) + { + if (data.Length != 4) + throw new ArgumentOutOfRangeException(nameof(data), data, "Session identifier must have 4 bytes"); - public string GetString() - { - return BitConverter.ToString(_sessionId); - } + Data = data; + } - public byte[] GetBytes() - { - var sessionIdSnapshot = new byte[4]; - Buffer.BlockCopy(_sessionId, 0, sessionIdSnapshot, 0, 4); - return sessionIdSnapshot; - } + public static implicit operator string(SessionId sessionId) + { + return BitConverter.ToString(sessionId.Data); + } - public void WriteTo(List list) - { - list.AddRange(_sessionId); - } + public static implicit operator byte[](SessionId sessionId) + { + return [..sessionId.Data]; + } - public override bool Equals(object? obj) - { - return obj is SessionId anotherSessionId && _sessionId.SequenceEqual(anotherSessionId._sessionId); - } + public override bool Equals(object? obj) + { + return obj is SessionId anotherSessionId + && Data.SequenceEqual(anotherSessionId.Data); + } - public override int GetHashCode() - { - return BitConverter.ToInt32(_sessionId, 0); - } - } + public override int GetHashCode() + { + return BitConverter.ToInt32(Data, 0); + } } diff --git a/sources/McQuery.Net/Exceptions/AlreadyExpiredException.cs b/sources/McQuery.Net/Exceptions/AlreadyExpiredException.cs new file mode 100644 index 0000000..129140a --- /dev/null +++ b/sources/McQuery.Net/Exceptions/AlreadyExpiredException.cs @@ -0,0 +1,16 @@ +using McQuery.Net.Abstract; + +namespace McQuery.Net.Exceptions; + +[PublicAPI] +public class AlreadyExpiredException : ArgumentException +{ + internal AlreadyExpiredException(IExpirable expirable) : base($"{expirable.GetType().Name} is already expired") + { + } + + internal static void ThrowIfExpired(IExpirable expirable) + { + if (expirable.IsExpired) throw new AlreadyExpiredException(expirable); + } +} \ No newline at end of file diff --git a/sources/McQuery.Net/GlobalUsings.cs b/sources/McQuery.Net/GlobalUsings.cs new file mode 100644 index 0000000..f3b5eaa --- /dev/null +++ b/sources/McQuery.Net/GlobalUsings.cs @@ -0,0 +1,3 @@ +// Global using directives + +global using JetBrains.Annotations; \ No newline at end of file diff --git a/sources/McQuery.Net/McQuery.Net.csproj b/sources/McQuery.Net/McQuery.Net.csproj index 69a6dd0..14f9fda 100644 --- a/sources/McQuery.Net/McQuery.Net.csproj +++ b/sources/McQuery.Net/McQuery.Net.csproj @@ -1,7 +1,11 @@  - - true - + + true + + + + + From 433fc42dd471dcfeaec3dcb415401cbc8574de85 Mon Sep 17 00:00:00 2001 From: Liven Maxim Date: Sat, 16 Nov 2024 20:24:37 +0300 Subject: [PATCH 03/31] feat: finish request forming --- sources/McQuery.Net/Data/ChallengeToken.cs | 7 ++- .../{BasicState.cs => BasicStatus.cs} | 6 +-- .../Responses/{FullState.cs => FullStatus.cs} | 6 +-- sources/McQuery.Net/Data/Session.cs | 6 +++ sources/McQuery.Net/Data/SessionId.cs | 32 ++++++------- .../McQuery.Net/Factories/IRequestFactory.cs | 10 +++++ .../McQuery.Net/Factories/RequestFactory.cs | 45 +++++++++++++++++++ sources/McQuery.Net/IMcQueryClient.cs | 14 ++++++ sources/McQuery.Net/McQuery.Net.csproj | 4 ++ sources/McQuery.Net/McQueryClient.cs | 26 +++++++++++ .../Providers/ISessionIdProvider.cs | 8 ++++ .../Providers/SessionIdProvider.cs | 21 +++++++++ 12 files changed, 156 insertions(+), 29 deletions(-) rename sources/McQuery.Net/Data/Responses/{BasicState.cs => BasicStatus.cs} (79%) rename sources/McQuery.Net/Data/Responses/{FullState.cs => FullStatus.cs} (85%) create mode 100644 sources/McQuery.Net/Data/Session.cs create mode 100644 sources/McQuery.Net/Factories/IRequestFactory.cs create mode 100644 sources/McQuery.Net/Factories/RequestFactory.cs create mode 100644 sources/McQuery.Net/IMcQueryClient.cs create mode 100644 sources/McQuery.Net/McQueryClient.cs create mode 100644 sources/McQuery.Net/Providers/ISessionIdProvider.cs create mode 100644 sources/McQuery.Net/Providers/SessionIdProvider.cs diff --git a/sources/McQuery.Net/Data/ChallengeToken.cs b/sources/McQuery.Net/Data/ChallengeToken.cs index ca06f7a..fd71687 100644 --- a/sources/McQuery.Net/Data/ChallengeToken.cs +++ b/sources/McQuery.Net/Data/ChallengeToken.cs @@ -6,7 +6,7 @@ namespace McQuery.Net.Data; internal record ChallengeToken : IExpirable { private const int AlivePeriod = 29; - private readonly DateTime _expiresAt = DateTime.UtcNow.AddSeconds(AlivePeriod); + private readonly DateTime expiresAt = DateTime.UtcNow.AddSeconds(AlivePeriod); /// /// .ctor. @@ -24,11 +24,14 @@ public ChallengeToken(byte[] data) } private byte[] Data { get; } - public bool IsExpired => DateTime.UtcNow >= _expiresAt; + public bool IsExpired => DateTime.UtcNow >= expiresAt; public static implicit operator byte[](ChallengeToken token) { AlreadyExpiredException.ThrowIfExpired(token); return [..token.Data]; } + + public static implicit operator ReadOnlySpan(ChallengeToken token) => + (byte[])token; } diff --git a/sources/McQuery.Net/Data/Responses/BasicState.cs b/sources/McQuery.Net/Data/Responses/BasicStatus.cs similarity index 79% rename from sources/McQuery.Net/Data/Responses/BasicState.cs rename to sources/McQuery.Net/Data/Responses/BasicStatus.cs index 1189bf2..add871e 100644 --- a/sources/McQuery.Net/Data/Responses/BasicState.cs +++ b/sources/McQuery.Net/Data/Responses/BasicStatus.cs @@ -1,10 +1,9 @@ namespace McQuery.Net.Data.Responses; /// -/// Represents data which is received from BasicState request. +/// Represents data which is received from BasicStatus request. /// /// -/// . /// Message of the day. /// Type of the game. /// Name of a map. @@ -13,8 +12,7 @@ namespace McQuery.Net.Data.Responses; /// Port to connect. /// Ip to connect. [PublicAPI] -public record BasicState( - SessionId SessionId, +public record BasicStatus( string Motd, string GameType, string Map, diff --git a/sources/McQuery.Net/Data/Responses/FullState.cs b/sources/McQuery.Net/Data/Responses/FullStatus.cs similarity index 85% rename from sources/McQuery.Net/Data/Responses/FullState.cs rename to sources/McQuery.Net/Data/Responses/FullStatus.cs index 741c0e3..b016400 100644 --- a/sources/McQuery.Net/Data/Responses/FullState.cs +++ b/sources/McQuery.Net/Data/Responses/FullStatus.cs @@ -1,10 +1,9 @@ namespace McQuery.Net.Data.Responses; /// -/// Represents data which is received from FullState request. +/// Represents data which is received from FullStatus request. /// /// -/// . /// Message of the day. /// Type of the game. /// Identifier of a game. Constant value: MINECRAFT. @@ -17,8 +16,7 @@ namespace McQuery.Net.Data.Responses; /// Port to connect. /// Ip to connect. [PublicAPI] -public record FullState( - SessionId SessionId, +public record FullStatus( string Motd, string GameType, string GameId, diff --git a/sources/McQuery.Net/Data/Session.cs b/sources/McQuery.Net/Data/Session.cs new file mode 100644 index 0000000..9e7b28a --- /dev/null +++ b/sources/McQuery.Net/Data/Session.cs @@ -0,0 +1,6 @@ +namespace McQuery.Net.Data; + +internal record Session(SessionId SessionId, ChallengeToken Token) +{ + public bool IsExpired => Token.IsExpired; +} diff --git a/sources/McQuery.Net/Data/SessionId.cs b/sources/McQuery.Net/Data/SessionId.cs index 76fb5ea..caa94ee 100644 --- a/sources/McQuery.Net/Data/SessionId.cs +++ b/sources/McQuery.Net/Data/SessionId.cs @@ -3,8 +3,7 @@ namespace McQuery.Net.Data; /// /// Represents Session Identifier. /// -[PublicAPI] -public class SessionId +internal class SessionId { private byte[] Data { get; } @@ -23,24 +22,19 @@ public SessionId(byte[] data) Data = data; } - public static implicit operator string(SessionId sessionId) - { - return BitConverter.ToString(sessionId.Data); - } + public static implicit operator string(SessionId sessionId) => + BitConverter.ToString(sessionId.Data); - public static implicit operator byte[](SessionId sessionId) - { - return [..sessionId.Data]; - } + public static implicit operator byte[](SessionId sessionId) => + [..sessionId.Data]; - public override bool Equals(object? obj) - { - return obj is SessionId anotherSessionId - && Data.SequenceEqual(anotherSessionId.Data); - } + public static implicit operator ReadOnlySpan(SessionId sessionId) => + (byte[])sessionId; - public override int GetHashCode() - { - return BitConverter.ToInt32(Data, 0); - } + public override bool Equals(object? obj) => + obj is SessionId anotherSessionId + && Data.SequenceEqual(anotherSessionId.Data); + + public override int GetHashCode() => + BitConverter.ToInt32(Data, 0); } diff --git a/sources/McQuery.Net/Factories/IRequestFactory.cs b/sources/McQuery.Net/Factories/IRequestFactory.cs new file mode 100644 index 0000000..a7baf9b --- /dev/null +++ b/sources/McQuery.Net/Factories/IRequestFactory.cs @@ -0,0 +1,10 @@ +using McQuery.Net.Data; + +namespace McQuery.Net.Factories; + +internal interface IRequestFactory +{ + internal byte[] GetHandshakeRequest(SessionId sessionId); + internal byte[] GetBasicStatusRequest(Session session); + internal byte[] GetFullStatusRequest(Session session); +} diff --git a/sources/McQuery.Net/Factories/RequestFactory.cs b/sources/McQuery.Net/Factories/RequestFactory.cs new file mode 100644 index 0000000..62e168d --- /dev/null +++ b/sources/McQuery.Net/Factories/RequestFactory.cs @@ -0,0 +1,45 @@ +using McQuery.Net.Data; + +namespace McQuery.Net.Factories; + +internal class RequestFactory : IRequestFactory +{ + private const byte HandshakeRequestTypeConst = 0x09; + private const byte StatusRequestTypeConst = 0x00; + private static readonly byte[] MagicConst = [0xfe, 0xfd]; + + public byte[] GetHandshakeRequest(SessionId sessionId) + { + using MemoryStream packetStream = new(); + FormRequestHeader(packetStream, HandshakeRequestTypeConst, sessionId); + return packetStream.ToArray(); + } + + public byte[] GetBasicStatusRequest(Session session) + { + using MemoryStream packetStream = new(); + FormBasicStatusRequest(packetStream, session); + return packetStream.ToArray(); + } + + public byte[] GetFullStatusRequest(Session session) + { + using MemoryStream packetStream = new(); + FormBasicStatusRequest(packetStream, session); + packetStream.Write([0x00, 0x00, 0x00, 0x00]); + return packetStream.ToArray(); + } + + private static void FormRequestHeader(Stream packetStream, byte packageType, SessionId sessionId) + { + packetStream.Write(MagicConst); + packetStream.Write([packageType]); + packetStream.Write(sessionId); + } + + private void FormBasicStatusRequest(Stream packetStream, Session session) + { + FormRequestHeader(packetStream, StatusRequestTypeConst, session.SessionId); + packetStream.Write(session.Token); + } +} diff --git a/sources/McQuery.Net/IMcQueryClient.cs b/sources/McQuery.Net/IMcQueryClient.cs new file mode 100644 index 0000000..6966bd7 --- /dev/null +++ b/sources/McQuery.Net/IMcQueryClient.cs @@ -0,0 +1,14 @@ +using System.Net; +using McQuery.Net.Data.Responses; + +namespace McQuery.Net; + +[PublicAPI] +public interface IMcQueryClient +{ + Task GetBasicStatusAsync(IPEndPoint serverEndPoint, CancellationToken cancellationToken = default); + BasicStatus GetBasicStatus(IPEndPoint serverEndPoint, CancellationToken cancellationToken = default); + + Task GetFullStatusAsync(IPEndPoint serverEndPoint, CancellationToken cancellationToken = default); + FullStatus GetFullStatus(IPEndPoint serverEndPoint, CancellationToken cancellationToken = default); +} diff --git a/sources/McQuery.Net/McQuery.Net.csproj b/sources/McQuery.Net/McQuery.Net.csproj index 14f9fda..3196a44 100644 --- a/sources/McQuery.Net/McQuery.Net.csproj +++ b/sources/McQuery.Net/McQuery.Net.csproj @@ -7,5 +7,9 @@ + + + + diff --git a/sources/McQuery.Net/McQueryClient.cs b/sources/McQuery.Net/McQueryClient.cs new file mode 100644 index 0000000..270a164 --- /dev/null +++ b/sources/McQuery.Net/McQueryClient.cs @@ -0,0 +1,26 @@ +using System.Net; +using McQuery.Net.Data.Responses; + +namespace McQuery.Net; + +[PublicAPI] +public class McQueryClient : IMcQueryClient +{ + public async Task GetBasicStatusAsync(IPEndPoint serverEndPoint, CancellationToken cancellationToken = default) + { + await Task.Yield(); + throw new NotImplementedException(); + } + + public async Task GetFullStatusAsync(IPEndPoint serverEndPoint, CancellationToken cancellationToken = default) + { + await Task.Yield(); + throw new NotImplementedException(); + } + + public BasicStatus GetBasicStatus(IPEndPoint serverEndPoint, CancellationToken cancellationToken = default) => + GetBasicStatusAsync(serverEndPoint, cancellationToken).GetAwaiter().GetResult(); + + public FullStatus GetFullStatus(IPEndPoint serverEndPoint, CancellationToken cancellationToken = default) => + GetFullStatusAsync(serverEndPoint, cancellationToken).GetAwaiter().GetResult(); +} diff --git a/sources/McQuery.Net/Providers/ISessionIdProvider.cs b/sources/McQuery.Net/Providers/ISessionIdProvider.cs new file mode 100644 index 0000000..04818ff --- /dev/null +++ b/sources/McQuery.Net/Providers/ISessionIdProvider.cs @@ -0,0 +1,8 @@ +using McQuery.Net.Data; + +namespace McQuery.Net.Providers; + +internal interface ISessionIdProvider +{ + SessionId Get(); +} diff --git a/sources/McQuery.Net/Providers/SessionIdProvider.cs b/sources/McQuery.Net/Providers/SessionIdProvider.cs new file mode 100644 index 0000000..e0335e6 --- /dev/null +++ b/sources/McQuery.Net/Providers/SessionIdProvider.cs @@ -0,0 +1,21 @@ +using McQuery.Net.Data; + +namespace McQuery.Net.Providers; + +internal class SessionIdProvider : ISessionIdProvider +{ + private static uint counter; + + public SessionId Get() + { + uint currentValue = Interlocked.Increment(ref counter); + + byte[] bytes = BitConverter.GetBytes(currentValue); + if (BitConverter.IsLittleEndian) + { + Array.Reverse(bytes); + } + + return new SessionId(bytes); + } +} From 9aac6151e02a8b6a94a4241b8d1c13fc6546bfdf Mon Sep 17 00:00:00 2001 From: Liven Maxim Date: Sat, 16 Nov 2024 23:51:53 +0300 Subject: [PATCH 04/31] feat: basic status implementation --- .../McQuery.Net/Abstract/IAuthOnlyClient.cs | 13 ++ .../{ => Data}/Factories/IRequestFactory.cs | 4 +- .../{ => Data}/Factories/RequestFactory.cs | 4 +- .../Data/Parsers/IResponseParser.cs | 104 ++++++++++++++++ .../Providers/ISessionIdProvider.cs | 4 +- .../Data/Providers/ISessionStorage.cs | 8 ++ .../{ => Data}/Providers/SessionIdProvider.cs | 4 +- .../Data/Providers/SessionStorage.cs | 70 +++++++++++ .../McQuery.Net/Data/Responses/BasicStatus.cs | 8 ++ .../McQuery.Net/Data/Responses/FullStatus.cs | 8 ++ .../McQuery.Net/Data/Responses/StatusBase.cs | 14 +++ sources/McQuery.Net/IMcQueryClient.cs | 10 +- sources/McQuery.Net/IMcQueryClientFactory.cs | 28 +++++ sources/McQuery.Net/McQueryClient.cs | 114 ++++++++++++++++-- 14 files changed, 365 insertions(+), 28 deletions(-) create mode 100644 sources/McQuery.Net/Abstract/IAuthOnlyClient.cs rename sources/McQuery.Net/{ => Data}/Factories/IRequestFactory.cs (79%) rename sources/McQuery.Net/{ => Data}/Factories/RequestFactory.cs (96%) create mode 100644 sources/McQuery.Net/Data/Parsers/IResponseParser.cs rename sources/McQuery.Net/{ => Data}/Providers/ISessionIdProvider.cs (52%) create mode 100644 sources/McQuery.Net/Data/Providers/ISessionStorage.cs rename sources/McQuery.Net/{ => Data}/Providers/SessionIdProvider.cs (87%) create mode 100644 sources/McQuery.Net/Data/Providers/SessionStorage.cs create mode 100644 sources/McQuery.Net/Data/Responses/StatusBase.cs create mode 100644 sources/McQuery.Net/IMcQueryClientFactory.cs diff --git a/sources/McQuery.Net/Abstract/IAuthOnlyClient.cs b/sources/McQuery.Net/Abstract/IAuthOnlyClient.cs new file mode 100644 index 0000000..749c4a1 --- /dev/null +++ b/sources/McQuery.Net/Abstract/IAuthOnlyClient.cs @@ -0,0 +1,13 @@ +using System.Net; +using McQuery.Net.Data; + +namespace McQuery.Net.Abstract; + +internal interface IAuthOnlyClient : IDisposable +{ + + internal Task HandshakeAsync( + IPEndPoint serverEndpoint, + SessionId sessionId, + CancellationToken cancellationToken = default); +} diff --git a/sources/McQuery.Net/Factories/IRequestFactory.cs b/sources/McQuery.Net/Data/Factories/IRequestFactory.cs similarity index 79% rename from sources/McQuery.Net/Factories/IRequestFactory.cs rename to sources/McQuery.Net/Data/Factories/IRequestFactory.cs index a7baf9b..6d861ec 100644 --- a/sources/McQuery.Net/Factories/IRequestFactory.cs +++ b/sources/McQuery.Net/Data/Factories/IRequestFactory.cs @@ -1,6 +1,4 @@ -using McQuery.Net.Data; - -namespace McQuery.Net.Factories; +namespace McQuery.Net.Data.Factories; internal interface IRequestFactory { diff --git a/sources/McQuery.Net/Factories/RequestFactory.cs b/sources/McQuery.Net/Data/Factories/RequestFactory.cs similarity index 96% rename from sources/McQuery.Net/Factories/RequestFactory.cs rename to sources/McQuery.Net/Data/Factories/RequestFactory.cs index 62e168d..4d95a45 100644 --- a/sources/McQuery.Net/Factories/RequestFactory.cs +++ b/sources/McQuery.Net/Data/Factories/RequestFactory.cs @@ -1,6 +1,4 @@ -using McQuery.Net.Data; - -namespace McQuery.Net.Factories; +namespace McQuery.Net.Data.Factories; internal class RequestFactory : IRequestFactory { diff --git a/sources/McQuery.Net/Data/Parsers/IResponseParser.cs b/sources/McQuery.Net/Data/Parsers/IResponseParser.cs new file mode 100644 index 0000000..7290788 --- /dev/null +++ b/sources/McQuery.Net/Data/Parsers/IResponseParser.cs @@ -0,0 +1,104 @@ +using System.Buffers; +using System.Text; +using McQuery.Net.Data.Responses; + +namespace McQuery.Net.Data.Parsers; + +internal interface IResponseParser +{ + T Parse(byte[] data); +} + +internal abstract class ResponseParserBase +{ + public abstract byte ResponseType { get; } + + public SessionId StartParsing(byte[] data, out SequenceReader reader) + { + ReadOnlySequence sequence = new(data); + reader = new SequenceReader(sequence); + + if (!reader.IsNext([ResponseType], true)) + { + throw new InvalidOperationException("Invalid response type"); + } + + return ParseSessionId(ref reader); + } + + private static SessionId ParseSessionId(ref SequenceReader reader) + { + if (reader.UnreadSequence.Length < 4) + { + throw new InvalidOperationException("Session id must contain exactly 4 bytes."); + } + + reader.TryReadExact(4, out ReadOnlySequence sessionIdBytes); + + return new SessionId(sessionIdBytes.ToArray()); + } + + internal static string ParseNullTerminatingString(ref SequenceReader reader) + { + if (!reader.TryReadTo(out ReadOnlySequence bytes, 0, true)) + throw new InvalidOperationException("Cannot parse null terminating string: terminator was not found."); + + return Encoding.ASCII.GetString(bytes); + } + + internal static short ParseShortLittleEndian(ref SequenceReader reader) + { + if (!reader.TryReadLittleEndian(out short port)) + throw new InvalidOperationException("Cannot parse short value"); + + return port; + } +} + +internal class HandshakeResponseParser : ResponseParserBase, IResponseParser +{ + public override byte ResponseType => 0x09; + + public ChallengeToken Parse(byte[] data) + { + StartParsing(data, out SequenceReader reader); + + string challengeTokenString = ParseNullTerminatingString(ref reader); + byte[] challengeTokenBytes = BitConverter.GetBytes(int.Parse(challengeTokenString)); + + if (BitConverter.IsLittleEndian) + { + Array.Reverse(challengeTokenBytes); + } + + return new ChallengeToken(challengeTokenBytes); + } +} + +internal abstract class StatusResponseParser : ResponseParserBase, IResponseParser where T : StatusBase +{ + public override byte ResponseType => 0x00; + + public abstract T Parse(byte[] data); +} + +internal class BasicStatusResponseParser : StatusResponseParser +{ + public override BasicStatus Parse(byte[] data) + { + SessionId sessionId = StartParsing(data, out SequenceReader reader); + + return new BasicStatus( + ParseNullTerminatingString(ref reader), + ParseNullTerminatingString(ref reader), + ParseNullTerminatingString(ref reader), + int.Parse(ParseNullTerminatingString(ref reader)), + int.Parse(ParseNullTerminatingString(ref reader)), + ParseShortLittleEndian(ref reader), + ParseNullTerminatingString(ref reader) + ) + { + SessionId = sessionId + }; + } +} diff --git a/sources/McQuery.Net/Providers/ISessionIdProvider.cs b/sources/McQuery.Net/Data/Providers/ISessionIdProvider.cs similarity index 52% rename from sources/McQuery.Net/Providers/ISessionIdProvider.cs rename to sources/McQuery.Net/Data/Providers/ISessionIdProvider.cs index 04818ff..c769bbf 100644 --- a/sources/McQuery.Net/Providers/ISessionIdProvider.cs +++ b/sources/McQuery.Net/Data/Providers/ISessionIdProvider.cs @@ -1,6 +1,4 @@ -using McQuery.Net.Data; - -namespace McQuery.Net.Providers; +namespace McQuery.Net.Data.Providers; internal interface ISessionIdProvider { diff --git a/sources/McQuery.Net/Data/Providers/ISessionStorage.cs b/sources/McQuery.Net/Data/Providers/ISessionStorage.cs new file mode 100644 index 0000000..d603231 --- /dev/null +++ b/sources/McQuery.Net/Data/Providers/ISessionStorage.cs @@ -0,0 +1,8 @@ +using System.Net; + +namespace McQuery.Net.Data.Providers; + +internal interface ISessionStorage: IDisposable +{ + Task GetAsync(IPEndPoint serverEndpoint, CancellationToken token = default); +} diff --git a/sources/McQuery.Net/Providers/SessionIdProvider.cs b/sources/McQuery.Net/Data/Providers/SessionIdProvider.cs similarity index 87% rename from sources/McQuery.Net/Providers/SessionIdProvider.cs rename to sources/McQuery.Net/Data/Providers/SessionIdProvider.cs index e0335e6..91eaf6e 100644 --- a/sources/McQuery.Net/Providers/SessionIdProvider.cs +++ b/sources/McQuery.Net/Data/Providers/SessionIdProvider.cs @@ -1,6 +1,4 @@ -using McQuery.Net.Data; - -namespace McQuery.Net.Providers; +namespace McQuery.Net.Data.Providers; internal class SessionIdProvider : ISessionIdProvider { diff --git a/sources/McQuery.Net/Data/Providers/SessionStorage.cs b/sources/McQuery.Net/Data/Providers/SessionStorage.cs new file mode 100644 index 0000000..3c114eb --- /dev/null +++ b/sources/McQuery.Net/Data/Providers/SessionStorage.cs @@ -0,0 +1,70 @@ +using System.Collections.Concurrent; +using System.Net; +using McQuery.Net.Abstract; + +namespace McQuery.Net.Data.Providers; + +internal class SessionStorage(ISessionIdProvider sessionIdProvider) : ISessionStorage +{ + private IAuthOnlyClient? authClient; + private readonly ConcurrentDictionary sessionsByEndpoints = new(); + + public async Task GetAsync(IPEndPoint serverEndpoint, CancellationToken cancellationToken = default) + { + bool sessionExists = sessionsByEndpoints.TryGetValue(serverEndpoint, out Session? session); + + if (sessionExists && !session!.IsExpired) + { + return session; + } + + if (sessionExists && session!.IsExpired) + { + if (!sessionsByEndpoints.TryRemove(serverEndpoint, out _)) + { + throw new Exception($"Cannot remove expired session {session} for some reason."); + } + } + + return await AcquireSession(serverEndpoint, cancellationToken); + } + + internal void Init(IAuthOnlyClient client) + { + if (authClient != null) + { + throw new InvalidOperationException("SessionStorage already initialized."); + } + + authClient = client; + } + + private async Task AcquireSession(IPEndPoint serverEndpoint, CancellationToken cancellationToken) + { + if (authClient == null) + { + throw new InvalidOperationException("Storage must be initialized before calling this method."); + } + + SessionId sessionId = sessionIdProvider.Get(); + ChallengeToken challengeToken = await authClient!.HandshakeAsync(serverEndpoint, sessionId, cancellationToken); + Session session = new(sessionId, challengeToken); + + if (!sessionsByEndpoints.TryAdd(serverEndpoint, session)) + { + throw new Exception("Cannot add session for endpoint " + serverEndpoint + " for some reason."); + } + + return session; + } + + private bool isDisposed; + public void Dispose() + { + if(isDisposed) return; + + authClient?.Dispose(); + GC.SuppressFinalize(this); + isDisposed = true; + } +} diff --git a/sources/McQuery.Net/Data/Responses/BasicStatus.cs b/sources/McQuery.Net/Data/Responses/BasicStatus.cs index add871e..33d3ee5 100644 --- a/sources/McQuery.Net/Data/Responses/BasicStatus.cs +++ b/sources/McQuery.Net/Data/Responses/BasicStatus.cs @@ -20,4 +20,12 @@ public record BasicStatus( int MaxPlayers, int HostPort, string HostIp +) : StatusBase( + Motd, + GameType, + Map, + NumPlayers, + MaxPlayers, + HostPort, + HostIp ); diff --git a/sources/McQuery.Net/Data/Responses/FullStatus.cs b/sources/McQuery.Net/Data/Responses/FullStatus.cs index b016400..961dfe4 100644 --- a/sources/McQuery.Net/Data/Responses/FullStatus.cs +++ b/sources/McQuery.Net/Data/Responses/FullStatus.cs @@ -28,4 +28,12 @@ public record FullStatus( string[] PlayerList, int HostPort, string HostIp +) : StatusBase( + Motd, + GameType, + Map, + NumPlayers, + MaxPlayers, + HostPort, + HostIp ); diff --git a/sources/McQuery.Net/Data/Responses/StatusBase.cs b/sources/McQuery.Net/Data/Responses/StatusBase.cs new file mode 100644 index 0000000..bf42700 --- /dev/null +++ b/sources/McQuery.Net/Data/Responses/StatusBase.cs @@ -0,0 +1,14 @@ +namespace McQuery.Net.Data.Responses; + +[PublicAPI] +public record StatusBase( + string Motd, + string GameType, + string Map, + int NumPlayers, + int MaxPlayers, + int HostPort, + string HostIp) +{ + internal SessionId SessionId { get; init; } = null!; +} diff --git a/sources/McQuery.Net/IMcQueryClient.cs b/sources/McQuery.Net/IMcQueryClient.cs index 6966bd7..1426ebc 100644 --- a/sources/McQuery.Net/IMcQueryClient.cs +++ b/sources/McQuery.Net/IMcQueryClient.cs @@ -4,11 +4,11 @@ namespace McQuery.Net; [PublicAPI] -public interface IMcQueryClient +public interface IMcQueryClient : IDisposable { - Task GetBasicStatusAsync(IPEndPoint serverEndPoint, CancellationToken cancellationToken = default); - BasicStatus GetBasicStatus(IPEndPoint serverEndPoint, CancellationToken cancellationToken = default); + Task GetBasicStatusAsync(IPEndPoint serverEndpoint, CancellationToken cancellationToken = default); + BasicStatus GetBasicStatus(IPEndPoint serverEndpoint, CancellationToken cancellationToken = default); - Task GetFullStatusAsync(IPEndPoint serverEndPoint, CancellationToken cancellationToken = default); - FullStatus GetFullStatus(IPEndPoint serverEndPoint, CancellationToken cancellationToken = default); + Task GetFullStatusAsync(IPEndPoint serverEndpoint, CancellationToken cancellationToken = default); + FullStatus GetFullStatus(IPEndPoint serverEndpoint, CancellationToken cancellationToken = default); } diff --git a/sources/McQuery.Net/IMcQueryClientFactory.cs b/sources/McQuery.Net/IMcQueryClientFactory.cs new file mode 100644 index 0000000..459fcb3 --- /dev/null +++ b/sources/McQuery.Net/IMcQueryClientFactory.cs @@ -0,0 +1,28 @@ +using System.Net.Sockets; +using McQuery.Net.Data.Factories; +using McQuery.Net.Data.Providers; + +namespace McQuery.Net; + +public interface IMcQueryClientFactory +{ + IMcQueryClient Get(); +} + +public class McQueryClientFactory : IMcQueryClientFactory +{ + private readonly Lazy sessionIdProvider = new(() => new SessionIdProvider(), isThreadSafe: true); + + public IMcQueryClient Get() + { + SessionStorage sessionStorage = new(sessionIdProvider.Value); + + McQueryClient client = new( + new UdpClient(), + new RequestFactory(), + sessionStorage); + sessionStorage.Init(client); + + return client; + } +} diff --git a/sources/McQuery.Net/McQueryClient.cs b/sources/McQuery.Net/McQueryClient.cs index 270a164..5a18ff7 100644 --- a/sources/McQuery.Net/McQueryClient.cs +++ b/sources/McQuery.Net/McQueryClient.cs @@ -1,26 +1,118 @@ using System.Net; +using System.Net.Sockets; +using McQuery.Net.Abstract; +using McQuery.Net.Data; +using McQuery.Net.Data.Factories; +using McQuery.Net.Data.Parsers; +using McQuery.Net.Data.Providers; using McQuery.Net.Data.Responses; namespace McQuery.Net; [PublicAPI] -public class McQueryClient : IMcQueryClient +public class McQueryClient : IMcQueryClient, IAuthOnlyClient { - public async Task GetBasicStatusAsync(IPEndPoint serverEndPoint, CancellationToken cancellationToken = default) + private readonly UdpClient socket; + private readonly IRequestFactory requestFactory; + private readonly ISessionStorage sessionStorage; + private int responseTimeoutSeconds = 5; // Temp value + + private static readonly IResponseParser HandshakeResponseParser = new HandshakeResponseParser(); + private static readonly IResponseParser BasicStatusResponseParser = new BasicStatusResponseParser(); + private static readonly IResponseParser FullStatusResponseParser = new ThrowResponseParser(); + + internal McQueryClient(UdpClient socket, IRequestFactory requestFactory, ISessionStorage sessionStorage) + { + this.requestFactory = requestFactory; + this.sessionStorage = sessionStorage; + this.socket = socket; + } + + public async Task GetBasicStatusAsync(IPEndPoint serverEndpoint, CancellationToken cancellationToken = default) => + await SendRequestAsync( + serverEndpoint, + session => requestFactory.GetBasicStatusRequest(session), + BasicStatusResponseParser, + cancellationToken); + + public async Task GetFullStatusAsync(IPEndPoint serverEndpoint, CancellationToken cancellationToken = default) => + await SendRequestAsync( + serverEndpoint, + session => requestFactory.GetFullStatusRequest(session), + FullStatusResponseParser, + cancellationToken); + + public BasicStatus GetBasicStatus(IPEndPoint serverEndpoint, CancellationToken cancellationToken = default) => + GetBasicStatusAsync(serverEndpoint, cancellationToken).GetAwaiter().GetResult(); + + public FullStatus GetFullStatus(IPEndPoint serverEndpoint, CancellationToken cancellationToken = default) => + GetFullStatusAsync(serverEndpoint, cancellationToken).GetAwaiter().GetResult(); + + async Task IAuthOnlyClient.HandshakeAsync( + IPEndPoint serverEndpoint, + SessionId sessionId, + CancellationToken cancellationToken) + { + byte[] packet = requestFactory.GetHandshakeRequest(sessionId); + + return await SendRequestAsync( + serverEndpoint, + packet, + HandshakeResponseParser, + cancellationToken); + } + + private async Task SendRequestAsync( + IPEndPoint serverEndpoint, + Func> packetFactory, + IResponseParser responseParser, + CancellationToken cancellationToken = default) { - await Task.Yield(); - throw new NotImplementedException(); + Session session = await sessionStorage.GetAsync(serverEndpoint, cancellationToken); + ReadOnlyMemory packet = packetFactory(session); + return await SendRequestAsync(serverEndpoint, packet, responseParser, cancellationToken); } - public async Task GetFullStatusAsync(IPEndPoint serverEndPoint, CancellationToken cancellationToken = default) + private async Task SendRequestAsync( + IPEndPoint serverEndpoint, + ReadOnlyMemory packet, + IResponseParser responseParser, + CancellationToken cancellationToken = default) { - await Task.Yield(); - throw new NotImplementedException(); + Console.WriteLine($"Sending {packet.Length} bytes to {serverEndpoint} with content {BitConverter.ToString(packet.ToArray())}"); + + await socket.SendAsync(packet, serverEndpoint, cancellationToken).ConfigureAwait(false); + + using CancellationTokenSource timeoutSource = new(TimeSpan.FromSeconds(responseTimeoutSeconds)); + using CancellationTokenSource linkedSource = + CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutSource.Token); + + CancellationToken tokenWithTimeout = linkedSource.Token; + // TODO: common response pool + UdpReceiveResult response = await socket.ReceiveAsync(tokenWithTimeout).ConfigureAwait(false); + + Console.WriteLine($"Received response from server: {BitConverter.ToString(response.Buffer)}"); + T responseData = responseParser.Parse(response.Buffer); + Console.WriteLine(responseData); + + return responseData; } - public BasicStatus GetBasicStatus(IPEndPoint serverEndPoint, CancellationToken cancellationToken = default) => - GetBasicStatusAsync(serverEndPoint, cancellationToken).GetAwaiter().GetResult(); - public FullStatus GetFullStatus(IPEndPoint serverEndPoint, CancellationToken cancellationToken = default) => - GetFullStatusAsync(serverEndPoint, cancellationToken).GetAwaiter().GetResult(); + private bool isDisposed; + public void Dispose() + { + if(isDisposed) return; + + socket.Dispose(); + sessionStorage.Dispose(); + GC.SuppressFinalize(this); + isDisposed = true; + } +} + +// TODO: remove +internal class ThrowResponseParser : IResponseParser +{ + public T Parse(byte[] data) => throw new NotImplementedException(); } From 36a126e2a0c1060317ff55b7d50e8eb46a3cfc16 Mon Sep 17 00:00:00 2001 From: Liven Maxim Date: Sat, 16 Nov 2024 23:56:40 +0300 Subject: [PATCH 05/31] feat: create template for full status parser --- .../Data/Parsers/BasicStatusResponseParser.cs | 25 +++++ .../Data/Parsers/FullStatusResponseParser.cs | 18 ++++ .../Data/Parsers/HandshakeResponseParser.cs | 23 +++++ .../Data/Parsers/IResponseParser.cs | 98 ------------------- .../Data/Parsers/ResponseParserBase.cs | 50 ++++++++++ .../Data/Parsers/StatusResponseParser.cs | 10 ++ sources/McQuery.Net/McQueryClient.cs | 8 +- 7 files changed, 127 insertions(+), 105 deletions(-) create mode 100644 sources/McQuery.Net/Data/Parsers/BasicStatusResponseParser.cs create mode 100644 sources/McQuery.Net/Data/Parsers/FullStatusResponseParser.cs create mode 100644 sources/McQuery.Net/Data/Parsers/HandshakeResponseParser.cs create mode 100644 sources/McQuery.Net/Data/Parsers/ResponseParserBase.cs create mode 100644 sources/McQuery.Net/Data/Parsers/StatusResponseParser.cs diff --git a/sources/McQuery.Net/Data/Parsers/BasicStatusResponseParser.cs b/sources/McQuery.Net/Data/Parsers/BasicStatusResponseParser.cs new file mode 100644 index 0000000..2d3de9b --- /dev/null +++ b/sources/McQuery.Net/Data/Parsers/BasicStatusResponseParser.cs @@ -0,0 +1,25 @@ +using System.Buffers; +using McQuery.Net.Data.Responses; + +namespace McQuery.Net.Data.Parsers; + +internal class BasicStatusResponseParser : StatusResponseParser +{ + public override BasicStatus Parse(byte[] data) + { + SessionId sessionId = StartParsing(data, out SequenceReader reader); + + return new BasicStatus( + ParseNullTerminatingString(ref reader), + ParseNullTerminatingString(ref reader), + ParseNullTerminatingString(ref reader), + int.Parse(ParseNullTerminatingString(ref reader)), + int.Parse(ParseNullTerminatingString(ref reader)), + ParseShortLittleEndian(ref reader), + ParseNullTerminatingString(ref reader) + ) + { + SessionId = sessionId + }; + } +} diff --git a/sources/McQuery.Net/Data/Parsers/FullStatusResponseParser.cs b/sources/McQuery.Net/Data/Parsers/FullStatusResponseParser.cs new file mode 100644 index 0000000..ec8fa61 --- /dev/null +++ b/sources/McQuery.Net/Data/Parsers/FullStatusResponseParser.cs @@ -0,0 +1,18 @@ +using System.Buffers; +using McQuery.Net.Data.Responses; + +namespace McQuery.Net.Data.Parsers; + +internal class FullStatusResponseParser : StatusResponseParser +{ + private static readonly byte[] Constant1 = [0x73, 0x70, 0x6C, 0x69, 0x74, 0x6E, 0x75, 0x6D, 0x00, 0x80, 0x00]; + + private static readonly byte[] Constant2 = [0x01, 0x70, 0x6C, 0x61, 0x79, 0x65, 0x72, 0x5F, 0x00, 0x00]; + + public override FullStatus Parse(byte[] data) + { + SessionId sessionId = StartParsing(data, out SequenceReader reader); + + throw new NotImplementedException(); + } +} diff --git a/sources/McQuery.Net/Data/Parsers/HandshakeResponseParser.cs b/sources/McQuery.Net/Data/Parsers/HandshakeResponseParser.cs new file mode 100644 index 0000000..de5b188 --- /dev/null +++ b/sources/McQuery.Net/Data/Parsers/HandshakeResponseParser.cs @@ -0,0 +1,23 @@ +using System.Buffers; + +namespace McQuery.Net.Data.Parsers; + +internal class HandshakeResponseParser : ResponseParserBase, IResponseParser +{ + public override byte ResponseType => 0x09; + + public ChallengeToken Parse(byte[] data) + { + StartParsing(data, out SequenceReader reader); + + string challengeTokenString = ParseNullTerminatingString(ref reader); + byte[] challengeTokenBytes = BitConverter.GetBytes(int.Parse(challengeTokenString)); + + if (BitConverter.IsLittleEndian) + { + Array.Reverse(challengeTokenBytes); + } + + return new ChallengeToken(challengeTokenBytes); + } +} diff --git a/sources/McQuery.Net/Data/Parsers/IResponseParser.cs b/sources/McQuery.Net/Data/Parsers/IResponseParser.cs index 7290788..3f9d145 100644 --- a/sources/McQuery.Net/Data/Parsers/IResponseParser.cs +++ b/sources/McQuery.Net/Data/Parsers/IResponseParser.cs @@ -1,104 +1,6 @@ -using System.Buffers; -using System.Text; -using McQuery.Net.Data.Responses; - namespace McQuery.Net.Data.Parsers; internal interface IResponseParser { T Parse(byte[] data); } - -internal abstract class ResponseParserBase -{ - public abstract byte ResponseType { get; } - - public SessionId StartParsing(byte[] data, out SequenceReader reader) - { - ReadOnlySequence sequence = new(data); - reader = new SequenceReader(sequence); - - if (!reader.IsNext([ResponseType], true)) - { - throw new InvalidOperationException("Invalid response type"); - } - - return ParseSessionId(ref reader); - } - - private static SessionId ParseSessionId(ref SequenceReader reader) - { - if (reader.UnreadSequence.Length < 4) - { - throw new InvalidOperationException("Session id must contain exactly 4 bytes."); - } - - reader.TryReadExact(4, out ReadOnlySequence sessionIdBytes); - - return new SessionId(sessionIdBytes.ToArray()); - } - - internal static string ParseNullTerminatingString(ref SequenceReader reader) - { - if (!reader.TryReadTo(out ReadOnlySequence bytes, 0, true)) - throw new InvalidOperationException("Cannot parse null terminating string: terminator was not found."); - - return Encoding.ASCII.GetString(bytes); - } - - internal static short ParseShortLittleEndian(ref SequenceReader reader) - { - if (!reader.TryReadLittleEndian(out short port)) - throw new InvalidOperationException("Cannot parse short value"); - - return port; - } -} - -internal class HandshakeResponseParser : ResponseParserBase, IResponseParser -{ - public override byte ResponseType => 0x09; - - public ChallengeToken Parse(byte[] data) - { - StartParsing(data, out SequenceReader reader); - - string challengeTokenString = ParseNullTerminatingString(ref reader); - byte[] challengeTokenBytes = BitConverter.GetBytes(int.Parse(challengeTokenString)); - - if (BitConverter.IsLittleEndian) - { - Array.Reverse(challengeTokenBytes); - } - - return new ChallengeToken(challengeTokenBytes); - } -} - -internal abstract class StatusResponseParser : ResponseParserBase, IResponseParser where T : StatusBase -{ - public override byte ResponseType => 0x00; - - public abstract T Parse(byte[] data); -} - -internal class BasicStatusResponseParser : StatusResponseParser -{ - public override BasicStatus Parse(byte[] data) - { - SessionId sessionId = StartParsing(data, out SequenceReader reader); - - return new BasicStatus( - ParseNullTerminatingString(ref reader), - ParseNullTerminatingString(ref reader), - ParseNullTerminatingString(ref reader), - int.Parse(ParseNullTerminatingString(ref reader)), - int.Parse(ParseNullTerminatingString(ref reader)), - ParseShortLittleEndian(ref reader), - ParseNullTerminatingString(ref reader) - ) - { - SessionId = sessionId - }; - } -} diff --git a/sources/McQuery.Net/Data/Parsers/ResponseParserBase.cs b/sources/McQuery.Net/Data/Parsers/ResponseParserBase.cs new file mode 100644 index 0000000..c50c6cc --- /dev/null +++ b/sources/McQuery.Net/Data/Parsers/ResponseParserBase.cs @@ -0,0 +1,50 @@ +using System.Buffers; +using System.Text; + +namespace McQuery.Net.Data.Parsers; + +internal abstract class ResponseParserBase +{ + public abstract byte ResponseType { get; } + + public SessionId StartParsing(byte[] data, out SequenceReader reader) + { + ReadOnlySequence sequence = new(data); + reader = new SequenceReader(sequence); + + if (!reader.IsNext([ResponseType], true)) + { + throw new InvalidOperationException("Invalid response type"); + } + + return ParseSessionId(ref reader); + } + + private static SessionId ParseSessionId(ref SequenceReader reader) + { + if (reader.UnreadSequence.Length < 4) + { + throw new InvalidOperationException("Session id must contain exactly 4 bytes."); + } + + reader.TryReadExact(4, out ReadOnlySequence sessionIdBytes); + + return new SessionId(sessionIdBytes.ToArray()); + } + + internal static string ParseNullTerminatingString(ref SequenceReader reader) + { + if (!reader.TryReadTo(out ReadOnlySequence bytes, 0, true)) + throw new InvalidOperationException("Cannot parse null terminating string: terminator was not found."); + + return Encoding.ASCII.GetString(bytes); + } + + internal static short ParseShortLittleEndian(ref SequenceReader reader) + { + if (!reader.TryReadLittleEndian(out short port)) + throw new InvalidOperationException("Cannot parse short value"); + + return port; + } +} diff --git a/sources/McQuery.Net/Data/Parsers/StatusResponseParser.cs b/sources/McQuery.Net/Data/Parsers/StatusResponseParser.cs new file mode 100644 index 0000000..2c0de94 --- /dev/null +++ b/sources/McQuery.Net/Data/Parsers/StatusResponseParser.cs @@ -0,0 +1,10 @@ +using McQuery.Net.Data.Responses; + +namespace McQuery.Net.Data.Parsers; + +internal abstract class StatusResponseParser : ResponseParserBase, IResponseParser where T : StatusBase +{ + public override byte ResponseType => 0x00; + + public abstract T Parse(byte[] data); +} diff --git a/sources/McQuery.Net/McQueryClient.cs b/sources/McQuery.Net/McQueryClient.cs index 5a18ff7..79ed174 100644 --- a/sources/McQuery.Net/McQueryClient.cs +++ b/sources/McQuery.Net/McQueryClient.cs @@ -19,7 +19,7 @@ public class McQueryClient : IMcQueryClient, IAuthOnlyClient private static readonly IResponseParser HandshakeResponseParser = new HandshakeResponseParser(); private static readonly IResponseParser BasicStatusResponseParser = new BasicStatusResponseParser(); - private static readonly IResponseParser FullStatusResponseParser = new ThrowResponseParser(); + private static readonly IResponseParser FullStatusResponseParser = new FullStatusResponseParser(); internal McQueryClient(UdpClient socket, IRequestFactory requestFactory, ISessionStorage sessionStorage) { @@ -110,9 +110,3 @@ public void Dispose() isDisposed = true; } } - -// TODO: remove -internal class ThrowResponseParser : IResponseParser -{ - public T Parse(byte[] data) => throw new NotImplementedException(); -} From 2eea0ee497714e527ee9d6013fcc1a4294497acf Mon Sep 17 00:00:00 2001 From: Liven Maxim Date: Sun, 17 Nov 2024 00:02:33 +0300 Subject: [PATCH 06/31] feat: implement parsing of full status response --- .../Data/Parsers/FullStatusResponseParser.cs | 42 ++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/sources/McQuery.Net/Data/Parsers/FullStatusResponseParser.cs b/sources/McQuery.Net/Data/Parsers/FullStatusResponseParser.cs index ec8fa61..bc154b5 100644 --- a/sources/McQuery.Net/Data/Parsers/FullStatusResponseParser.cs +++ b/sources/McQuery.Net/Data/Parsers/FullStatusResponseParser.cs @@ -9,10 +9,50 @@ internal class FullStatusResponseParser : StatusResponseParser private static readonly byte[] Constant2 = [0x01, 0x70, 0x6C, 0x61, 0x79, 0x65, 0x72, 0x5F, 0x00, 0x00]; + private static readonly InvalidOperationException ResponseFormatError = new("Invalid full status response format"); + public override FullStatus Parse(byte[] data) { SessionId sessionId = StartParsing(data, out SequenceReader reader); - throw new NotImplementedException(); + if (!reader.IsNext(Constant1, true)) + { + throw ResponseFormatError; + } + + Dictionary statusKeyValues = new(); + while (!reader.IsNext(0, true)) + { + string key = ParseNullTerminatingString(ref reader); + string value = ParseNullTerminatingString(ref reader); + statusKeyValues.Add(key, value); + } + + if (!reader.IsNext(Constant2, true)) + { + throw ResponseFormatError; + } + + List players = []; + while (!reader.IsNext(0, true)) + { + players.Add(ParseNullTerminatingString(ref reader)); + } + + return new FullStatus( + statusKeyValues["hostname"], + statusKeyValues["gametype"], + statusKeyValues["game_id"], + statusKeyValues["version"], + statusKeyValues["plugins"], + statusKeyValues["map"], + int.Parse(statusKeyValues["numplayers"]), + int.Parse(statusKeyValues["maxplayers"]), + players.ToArray(), + int.Parse(statusKeyValues["hostport"]), + statusKeyValues["hostip"]) + { + SessionId = sessionId, + }; } } From e9217495649075e4086c2aeafa430863c1e2e0a6 Mon Sep 17 00:00:00 2001 From: Liven Maxim Date: Sun, 17 Nov 2024 02:15:35 +0300 Subject: [PATCH 07/31] docs: add some docs --- .../McQuery.Net/Abstract/IAuthOnlyClient.cs | 11 ++++++++- sources/McQuery.Net/Data/ChallengeToken.cs | 3 +++ .../Data/Factories/IRequestFactory.cs | 20 ++++++++++++++++ .../Data/Factories/RequestFactory.cs | 8 ++++++- .../Data/Providers/ISessionIdProvider.cs | 7 ++++++ .../Data/Providers/ISessionStorage.cs | 11 ++++++++- .../Data/Providers/SessionIdProvider.cs | 4 ++++ .../Data/Providers/SessionStorage.cs | 5 ++++ sources/McQuery.Net/Data/Session.cs | 8 +++++++ sources/McQuery.Net/Data/SessionId.cs | 7 ++++++ sources/McQuery.Net/IMcQueryClient.cs | 22 ++++++++++++++++++ sources/McQuery.Net/IMcQueryClientFactory.cs | 22 ------------------ sources/McQuery.Net/McQueryClient.cs | 18 +++++++++++---- sources/McQuery.Net/McQueryClientFactory.cs | 23 +++++++++++++++++++ 14 files changed, 139 insertions(+), 30 deletions(-) create mode 100644 sources/McQuery.Net/McQueryClientFactory.cs diff --git a/sources/McQuery.Net/Abstract/IAuthOnlyClient.cs b/sources/McQuery.Net/Abstract/IAuthOnlyClient.cs index 749c4a1..58f5612 100644 --- a/sources/McQuery.Net/Abstract/IAuthOnlyClient.cs +++ b/sources/McQuery.Net/Abstract/IAuthOnlyClient.cs @@ -3,9 +3,18 @@ namespace McQuery.Net.Abstract; +/// +/// Client that provides interface to acquire . +/// internal interface IAuthOnlyClient : IDisposable { - + /// + /// Request from Minecraft server. + /// + /// + /// . + /// . + /// . internal Task HandshakeAsync( IPEndPoint serverEndpoint, SessionId sessionId, diff --git a/sources/McQuery.Net/Data/ChallengeToken.cs b/sources/McQuery.Net/Data/ChallengeToken.cs index fd71687..3d542bc 100644 --- a/sources/McQuery.Net/Data/ChallengeToken.cs +++ b/sources/McQuery.Net/Data/ChallengeToken.cs @@ -3,6 +3,9 @@ namespace McQuery.Net.Data; +/// +/// Secret value provided by Minecraft server to issue status requests. +/// internal record ChallengeToken : IExpirable { private const int AlivePeriod = 29; diff --git a/sources/McQuery.Net/Data/Factories/IRequestFactory.cs b/sources/McQuery.Net/Data/Factories/IRequestFactory.cs index 6d861ec..9509018 100644 --- a/sources/McQuery.Net/Data/Factories/IRequestFactory.cs +++ b/sources/McQuery.Net/Data/Factories/IRequestFactory.cs @@ -1,8 +1,28 @@ namespace McQuery.Net.Data.Factories; +/// +/// Provides methods to build requests. +/// internal interface IRequestFactory { + /// + /// Builds handshake request. + /// + /// . + /// Binary representation of the request. internal byte[] GetHandshakeRequest(SessionId sessionId); + + /// + /// Builds basic status request. + /// + /// . + /// Binary representation of the request. internal byte[] GetBasicStatusRequest(Session session); + + /// + /// Builds full status request. + /// + /// . + /// Binary representation of the request. internal byte[] GetFullStatusRequest(Session session); } diff --git a/sources/McQuery.Net/Data/Factories/RequestFactory.cs b/sources/McQuery.Net/Data/Factories/RequestFactory.cs index 4d95a45..6094f41 100644 --- a/sources/McQuery.Net/Data/Factories/RequestFactory.cs +++ b/sources/McQuery.Net/Data/Factories/RequestFactory.cs @@ -1,11 +1,15 @@ namespace McQuery.Net.Data.Factories; +/// +/// Implementation of . +/// internal class RequestFactory : IRequestFactory { private const byte HandshakeRequestTypeConst = 0x09; private const byte StatusRequestTypeConst = 0x00; private static readonly byte[] MagicConst = [0xfe, 0xfd]; + /// public byte[] GetHandshakeRequest(SessionId sessionId) { using MemoryStream packetStream = new(); @@ -13,6 +17,7 @@ public byte[] GetHandshakeRequest(SessionId sessionId) return packetStream.ToArray(); } + /// public byte[] GetBasicStatusRequest(Session session) { using MemoryStream packetStream = new(); @@ -20,6 +25,7 @@ public byte[] GetBasicStatusRequest(Session session) return packetStream.ToArray(); } + /// public byte[] GetFullStatusRequest(Session session) { using MemoryStream packetStream = new(); @@ -35,7 +41,7 @@ private static void FormRequestHeader(Stream packetStream, byte packageType, Ses packetStream.Write(sessionId); } - private void FormBasicStatusRequest(Stream packetStream, Session session) + private static void FormBasicStatusRequest(Stream packetStream, Session session) { FormRequestHeader(packetStream, StatusRequestTypeConst, session.SessionId); packetStream.Write(session.Token); diff --git a/sources/McQuery.Net/Data/Providers/ISessionIdProvider.cs b/sources/McQuery.Net/Data/Providers/ISessionIdProvider.cs index c769bbf..a8b271f 100644 --- a/sources/McQuery.Net/Data/Providers/ISessionIdProvider.cs +++ b/sources/McQuery.Net/Data/Providers/ISessionIdProvider.cs @@ -1,6 +1,13 @@ namespace McQuery.Net.Data.Providers; +/// +/// Provides every time it's needed. +/// internal interface ISessionIdProvider { + /// + /// Gets . + /// + /// . SessionId Get(); } diff --git a/sources/McQuery.Net/Data/Providers/ISessionStorage.cs b/sources/McQuery.Net/Data/Providers/ISessionStorage.cs index d603231..3bba385 100644 --- a/sources/McQuery.Net/Data/Providers/ISessionStorage.cs +++ b/sources/McQuery.Net/Data/Providers/ISessionStorage.cs @@ -2,7 +2,16 @@ namespace McQuery.Net.Data.Providers; +/// +/// Creates and stores objects. +/// internal interface ISessionStorage: IDisposable { - Task GetAsync(IPEndPoint serverEndpoint, CancellationToken token = default); + /// + /// Gets stored session or acquire new. + /// + /// to access Minecraft server by UDP. + /// . + /// . + Task GetAsync(IPEndPoint serverEndpoint, CancellationToken cancellationToken = default); } diff --git a/sources/McQuery.Net/Data/Providers/SessionIdProvider.cs b/sources/McQuery.Net/Data/Providers/SessionIdProvider.cs index 91eaf6e..0f7ac6e 100644 --- a/sources/McQuery.Net/Data/Providers/SessionIdProvider.cs +++ b/sources/McQuery.Net/Data/Providers/SessionIdProvider.cs @@ -1,9 +1,13 @@ namespace McQuery.Net.Data.Providers; +/// +/// Implementation of . +/// internal class SessionIdProvider : ISessionIdProvider { private static uint counter; + /// public SessionId Get() { uint currentValue = Interlocked.Increment(ref counter); diff --git a/sources/McQuery.Net/Data/Providers/SessionStorage.cs b/sources/McQuery.Net/Data/Providers/SessionStorage.cs index 3c114eb..b6f9972 100644 --- a/sources/McQuery.Net/Data/Providers/SessionStorage.cs +++ b/sources/McQuery.Net/Data/Providers/SessionStorage.cs @@ -4,11 +4,16 @@ namespace McQuery.Net.Data.Providers; +/// +/// Implementation of . +/// +/// . internal class SessionStorage(ISessionIdProvider sessionIdProvider) : ISessionStorage { private IAuthOnlyClient? authClient; private readonly ConcurrentDictionary sessionsByEndpoints = new(); + /// public async Task GetAsync(IPEndPoint serverEndpoint, CancellationToken cancellationToken = default) { bool sessionExists = sessionsByEndpoints.TryGetValue(serverEndpoint, out Session? session); diff --git a/sources/McQuery.Net/Data/Session.cs b/sources/McQuery.Net/Data/Session.cs index 9e7b28a..4351de9 100644 --- a/sources/McQuery.Net/Data/Session.cs +++ b/sources/McQuery.Net/Data/Session.cs @@ -1,5 +1,13 @@ namespace McQuery.Net.Data; +/// +/// Represents a combination of and values. +/// +/// +/// Replica of something similar that Minecraft server use. +/// +/// . +/// . internal record Session(SessionId SessionId, ChallengeToken Token) { public bool IsExpired => Token.IsExpired; diff --git a/sources/McQuery.Net/Data/SessionId.cs b/sources/McQuery.Net/Data/SessionId.cs index caa94ee..6b96d33 100644 --- a/sources/McQuery.Net/Data/SessionId.cs +++ b/sources/McQuery.Net/Data/SessionId.cs @@ -3,6 +3,13 @@ namespace McQuery.Net.Data; /// /// Represents Session Identifier. /// +/// +/// Minecraft server does not validate this value but store along with as long as handshake session +/// for current issuer is alive. +/// Can be rewritten by new value if current client send another one handshake request. +/// Server sends stored in every response (even if status request contains different +/// compared to handshake request, response contains actual from the last handshake request). +/// internal class SessionId { private byte[] Data { get; } diff --git a/sources/McQuery.Net/IMcQueryClient.cs b/sources/McQuery.Net/IMcQueryClient.cs index 1426ebc..f2535b2 100644 --- a/sources/McQuery.Net/IMcQueryClient.cs +++ b/sources/McQuery.Net/IMcQueryClient.cs @@ -3,12 +3,34 @@ namespace McQuery.Net; +/// +/// Client to request minecraft server status by Minecraft Query Protocol. +/// [PublicAPI] public interface IMcQueryClient : IDisposable { + /// + /// Get . + /// + /// to access Minecraft server by UDP. + /// . + /// . Task GetBasicStatusAsync(IPEndPoint serverEndpoint, CancellationToken cancellationToken = default); + + /// BasicStatus GetBasicStatus(IPEndPoint serverEndpoint, CancellationToken cancellationToken = default); + /// + /// Get . + /// + /// + /// Minecraft server caches prepared full status response for some time. + /// + /// to access Minecraft server by UDP. + /// . + /// . Task GetFullStatusAsync(IPEndPoint serverEndpoint, CancellationToken cancellationToken = default); + + /// FullStatus GetFullStatus(IPEndPoint serverEndpoint, CancellationToken cancellationToken = default); } diff --git a/sources/McQuery.Net/IMcQueryClientFactory.cs b/sources/McQuery.Net/IMcQueryClientFactory.cs index 459fcb3..ecb3b24 100644 --- a/sources/McQuery.Net/IMcQueryClientFactory.cs +++ b/sources/McQuery.Net/IMcQueryClientFactory.cs @@ -1,28 +1,6 @@ -using System.Net.Sockets; -using McQuery.Net.Data.Factories; -using McQuery.Net.Data.Providers; - namespace McQuery.Net; public interface IMcQueryClientFactory { IMcQueryClient Get(); } - -public class McQueryClientFactory : IMcQueryClientFactory -{ - private readonly Lazy sessionIdProvider = new(() => new SessionIdProvider(), isThreadSafe: true); - - public IMcQueryClient Get() - { - SessionStorage sessionStorage = new(sessionIdProvider.Value); - - McQueryClient client = new( - new UdpClient(), - new RequestFactory(), - sessionStorage); - sessionStorage.Init(client); - - return client; - } -} diff --git a/sources/McQuery.Net/McQueryClient.cs b/sources/McQuery.Net/McQueryClient.cs index 79ed174..1c5c134 100644 --- a/sources/McQuery.Net/McQueryClient.cs +++ b/sources/McQuery.Net/McQueryClient.cs @@ -9,13 +9,16 @@ namespace McQuery.Net; +/// +/// Implementation of . +/// [PublicAPI] public class McQueryClient : IMcQueryClient, IAuthOnlyClient { private readonly UdpClient socket; private readonly IRequestFactory requestFactory; private readonly ISessionStorage sessionStorage; - private int responseTimeoutSeconds = 5; // Temp value + private int responseTimeoutSeconds = 5; // TODO private static readonly IResponseParser HandshakeResponseParser = new HandshakeResponseParser(); private static readonly IResponseParser BasicStatusResponseParser = new BasicStatusResponseParser(); @@ -28,26 +31,31 @@ internal McQueryClient(UdpClient socket, IRequestFactory requestFactory, ISessio this.socket = socket; } - public async Task GetBasicStatusAsync(IPEndPoint serverEndpoint, CancellationToken cancellationToken = default) => + /// + public async Task GetBasicStatusAsync(IPEndPoint serverEndpoint, CancellationToken cancellationToken) => await SendRequestAsync( serverEndpoint, session => requestFactory.GetBasicStatusRequest(session), BasicStatusResponseParser, cancellationToken); - public async Task GetFullStatusAsync(IPEndPoint serverEndpoint, CancellationToken cancellationToken = default) => + /// + public async Task GetFullStatusAsync(IPEndPoint serverEndpoint, CancellationToken cancellationToken) => await SendRequestAsync( serverEndpoint, session => requestFactory.GetFullStatusRequest(session), FullStatusResponseParser, cancellationToken); - public BasicStatus GetBasicStatus(IPEndPoint serverEndpoint, CancellationToken cancellationToken = default) => + /// + public BasicStatus GetBasicStatus(IPEndPoint serverEndpoint, CancellationToken cancellationToken) => GetBasicStatusAsync(serverEndpoint, cancellationToken).GetAwaiter().GetResult(); - public FullStatus GetFullStatus(IPEndPoint serverEndpoint, CancellationToken cancellationToken = default) => + /// + public FullStatus GetFullStatus(IPEndPoint serverEndpoint, CancellationToken cancellationToken) => GetFullStatusAsync(serverEndpoint, cancellationToken).GetAwaiter().GetResult(); + /// async Task IAuthOnlyClient.HandshakeAsync( IPEndPoint serverEndpoint, SessionId sessionId, diff --git a/sources/McQuery.Net/McQueryClientFactory.cs b/sources/McQuery.Net/McQueryClientFactory.cs new file mode 100644 index 0000000..caa6ba3 --- /dev/null +++ b/sources/McQuery.Net/McQueryClientFactory.cs @@ -0,0 +1,23 @@ +using System.Net.Sockets; +using McQuery.Net.Data.Factories; +using McQuery.Net.Data.Providers; + +namespace McQuery.Net; + +public class McQueryClientFactory : IMcQueryClientFactory +{ + private readonly Lazy sessionIdProvider = new(() => new SessionIdProvider(), isThreadSafe: true); + + public IMcQueryClient Get() + { + SessionStorage sessionStorage = new(sessionIdProvider.Value); + + McQueryClient client = new( + new UdpClient(), + new RequestFactory(), + sessionStorage); + sessionStorage.Init(client); + + return client; + } +} From fc643831bff3125589a466dbb6d6ba960fea4234 Mon Sep 17 00:00:00 2001 From: Maxim Liven Date: Mon, 18 Nov 2024 13:35:32 +0300 Subject: [PATCH 08/31] chore: empty lines --- sources/McQuery.Net/Exceptions/AlreadyExpiredException.cs | 2 +- sources/McQuery.Net/GlobalUsings.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sources/McQuery.Net/Exceptions/AlreadyExpiredException.cs b/sources/McQuery.Net/Exceptions/AlreadyExpiredException.cs index 129140a..c020ab4 100644 --- a/sources/McQuery.Net/Exceptions/AlreadyExpiredException.cs +++ b/sources/McQuery.Net/Exceptions/AlreadyExpiredException.cs @@ -13,4 +13,4 @@ internal static void ThrowIfExpired(IExpirable expirable) { if (expirable.IsExpired) throw new AlreadyExpiredException(expirable); } -} \ No newline at end of file +} diff --git a/sources/McQuery.Net/GlobalUsings.cs b/sources/McQuery.Net/GlobalUsings.cs index f3b5eaa..5db5d03 100644 --- a/sources/McQuery.Net/GlobalUsings.cs +++ b/sources/McQuery.Net/GlobalUsings.cs @@ -1,3 +1,3 @@ // Global using directives -global using JetBrains.Annotations; \ No newline at end of file +global using JetBrains.Annotations; From 3520dc4728aae227b330ad5a4cfea3c86606a8ba Mon Sep 17 00:00:00 2001 From: Maxim Liven Date: Mon, 18 Nov 2024 16:45:01 +0300 Subject: [PATCH 09/31] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ed72449..f4f4a1f 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# McQueryLib.Net +# McQuery.Net Library for .Net which implements Minecraft Query protocol. You can use it for getting statuses of a Minecraft server. # Example of using From 81e575252b0a9ca320c41d71d41454f6ff5f123f Mon Sep 17 00:00:00 2001 From: Maxim Liven Date: Tue, 19 Nov 2024 11:34:26 +0300 Subject: [PATCH 10/31] refactor: reorder and cleanup + editorconfig --- .editorconfig | 260 ++++++++++++++++++ Directory.Packages.props | 4 +- McQuery.Net.slnx | 4 +- README.md | 2 + .../Data/{Responses => }/BasicStatus.cs | 2 +- .../Data/{Responses => }/FullStatus.cs | 2 +- .../Data/Parsers/HandshakeResponseParser.cs | 23 -- .../Data/Parsers/StatusResponseParser.cs | 10 - .../Data/{Responses => }/StatusBase.cs | 4 +- .../Exceptions/AlreadyExpiredException.cs | 5 +- sources/McQuery.Net/IMcQueryClient.cs | 2 +- sources/McQuery.Net/IMcQueryClientFactory.cs | 1 + .../Abstract/IAuthOnlyClient.cs | 7 +- .../{ => Internal}/Abstract/IExpoirable.cs | 4 +- .../{ => Internal}/Data/ChallengeToken.cs | 14 +- .../{ => Internal}/Data/Session.cs | 2 +- .../{ => Internal}/Data/SessionId.cs | 24 +- .../Factories/IRequestFactory.cs | 4 +- .../Factories/RequestFactory.cs | 8 +- .../Parsers/BasicStatusResponseParser.cs | 9 +- .../Parsers/FullStatusResponseParser.cs | 33 +-- .../Parsers/HandshakeResponseParser.cs | 20 ++ .../Parsers/IResponseParser.cs | 2 +- .../Parsers/ResponseParserBase.cs | 20 +- .../Internal/Parsers/StatusResponseParser.cs | 11 + .../Providers/ISessionIdProvider.cs | 4 +- .../Providers/ISessionStorage.cs | 5 +- .../Providers/SessionIdProvider.cs | 13 +- .../Providers/SessionStorage.cs | 37 ++- sources/McQuery.Net/McQuery.Net.csproj | 22 +- sources/McQuery.Net/McQueryClient.cs | 100 ++++--- sources/McQuery.Net/McQueryClientFactory.cs | 9 +- 32 files changed, 472 insertions(+), 195 deletions(-) create mode 100644 .editorconfig rename sources/McQuery.Net/Data/{Responses => }/BasicStatus.cs (95%) rename sources/McQuery.Net/Data/{Responses => }/FullStatus.cs (96%) delete mode 100644 sources/McQuery.Net/Data/Parsers/HandshakeResponseParser.cs delete mode 100644 sources/McQuery.Net/Data/Parsers/StatusResponseParser.cs rename sources/McQuery.Net/Data/{Responses => }/StatusBase.cs (78%) rename sources/McQuery.Net/{ => Internal}/Abstract/IAuthOnlyClient.cs (82%) rename sources/McQuery.Net/{ => Internal}/Abstract/IExpoirable.cs (61%) rename sources/McQuery.Net/{ => Internal}/Data/ChallengeToken.cs (77%) rename sources/McQuery.Net/{ => Internal}/Data/Session.cs (92%) rename sources/McQuery.Net/{ => Internal}/Data/SessionId.cs (76%) rename sources/McQuery.Net/{Data => Internal}/Factories/IRequestFactory.cs (92%) rename sources/McQuery.Net/{Data => Internal}/Factories/RequestFactory.cs (89%) rename sources/McQuery.Net/{Data => Internal}/Parsers/BasicStatusResponseParser.cs (73%) rename sources/McQuery.Net/{Data => Internal}/Parsers/FullStatusResponseParser.cs (58%) create mode 100644 sources/McQuery.Net/Internal/Parsers/HandshakeResponseParser.cs rename sources/McQuery.Net/{Data => Internal}/Parsers/IResponseParser.cs (64%) rename sources/McQuery.Net/{Data => Internal}/Parsers/ResponseParserBase.cs (71%) create mode 100644 sources/McQuery.Net/Internal/Parsers/StatusResponseParser.cs rename sources/McQuery.Net/{Data => Internal}/Providers/ISessionIdProvider.cs (78%) rename sources/McQuery.Net/{Data => Internal}/Providers/ISessionStorage.cs (80%) rename sources/McQuery.Net/{Data => Internal}/Providers/SessionIdProvider.cs (51%) rename sources/McQuery.Net/{Data => Internal}/Providers/SessionStorage.cs (60%) diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..9f0ee2c --- /dev/null +++ b/.editorconfig @@ -0,0 +1,260 @@ +# EditorConfig is awesome: https://EditorConfig.org + +# top-most EditorConfig file +root = true + +[*] +indent_style = space +charset = utf-8 +end_of_line = lf +trim_trailing_whitespace = true +insert_final_newline = true + +# XML project files +[*.csproj] +indent_size = 2 + +resharper_space_before_self_closing = true + +# XML config files +[*.{props,targets,ruleset,config,nuspec,resx,vsixmanifest,vsct}] +indent_size = 2 + +# JSON files +[*.json] +indent_size = 2 + +# Shell script files +[*.sh] +indent_size = 2 + +# CSharp code style settings: +[*.cs] +indent_size = 4 + +# Non-private static fields are PascalCase +dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.severity = warning +dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.symbols = non_private_static_fields +dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.style = pascal_case_style + +dotnet_naming_symbols.non_private_static_fields.applicable_kinds = field +dotnet_naming_symbols.non_private_static_fields.applicable_accessibilities = public, protected, internal, protected_internal, private_protected +dotnet_naming_symbols.non_private_static_fields.required_modifiers = static + +# Private static fields are camelCase +dotnet_naming_rule.private_static_fields_should_be_camel_case.severity = warning +dotnet_naming_rule.private_static_fields_should_be_camel_case.symbols = private_static_fields +dotnet_naming_rule.private_static_fields_should_be_camel_case.style = camel_case_style + +dotnet_naming_symbols.private_static_fields.applicable_kinds = field +dotnet_naming_symbols.private_static_fields.applicable_accessibilities = private +dotnet_naming_symbols.private_static_fields.required_modifiers = static + +# Non-private readonly fields are PascalCase +dotnet_naming_rule.non_private_readonly_fields_should_be_pascal_case.severity = warning +dotnet_naming_rule.non_private_readonly_fields_should_be_pascal_case.symbols = non_private_readonly_fields +dotnet_naming_rule.non_private_readonly_fields_should_be_pascal_case.style = pascal_case_style + +dotnet_naming_symbols.non_private_readonly_fields.applicable_kinds = field +dotnet_naming_symbols.non_private_readonly_fields.applicable_accessibilities = public, protected, internal, protected_internal, private_protected +dotnet_naming_symbols.non_private_readonly_fields.required_modifiers = readonly + +# Private readonly fields with underscore +dotnet_naming_rule.private_members_with_underscore.symbols = private_fields +dotnet_naming_rule.private_members_with_underscore.style = prefix_underscore +dotnet_naming_rule.private_members_with_underscore.severity = warning + +dotnet_naming_symbols.private_fields.applicable_kinds = field +dotnet_naming_symbols.private_fields.applicable_accessibilities = private + +# Prefix style +dotnet_naming_style.prefix_underscore.capitalization = camel_case +dotnet_naming_style.prefix_underscore.required_prefix = _ + +# Constants are PascalCase +dotnet_naming_rule.local_constants_rule.severity = warning +dotnet_naming_rule.local_constants_rule.style = pascal_case_style +dotnet_naming_rule.local_constants_rule.symbols = local_constants_symbols + +dotnet_naming_symbols.private_constants_symbols.applicable_accessibilities = private +dotnet_naming_symbols.private_constants_symbols.applicable_kinds = field +dotnet_naming_symbols.private_constants_symbols.required_modifiers = const + +dotnet_naming_rule.private_constants_rule.severity = warning +dotnet_naming_rule.private_constants_rule.style = pascal_case_style +dotnet_naming_rule.private_constants_rule.symbols = private_constants_symbols + +dotnet_naming_symbols.local_constants_symbols.applicable_accessibilities = * +dotnet_naming_symbols.local_constants_symbols.applicable_kinds = local +dotnet_naming_symbols.local_constants_symbols.required_modifiers = const + +# Locals and parameters are camelCase +dotnet_naming_rule.locals_should_be_camel_case.severity = warning +dotnet_naming_rule.locals_should_be_camel_case.symbols = locals_and_parameters +dotnet_naming_rule.locals_should_be_camel_case.style = camel_case_style + +dotnet_naming_symbols.locals_and_parameters.applicable_kinds = parameter, local + +# Local functions are PascalCase +dotnet_naming_rule.local_functions_should_be_pascal_case.severity = warning +dotnet_naming_rule.local_functions_should_be_pascal_case.symbols = local_functions +dotnet_naming_rule.local_functions_should_be_pascal_case.style = pascal_case_style + +dotnet_naming_symbols.local_functions.applicable_kinds = local_function + +# Test methods are underscore tolerant +dotnet_naming_rule.test_methods_should_be_underscore_tolerant.severity = warning +dotnet_naming_rule.test_methods_should_be_underscore_tolerant.symbols = test_methods +dotnet_naming_rule.test_methods_should_be_underscore_tolerant.style = test_methods_style + +dotnet_naming_symbols.test_methods.applicable_accessibilities = local, public +dotnet_naming_symbols.test_methods.applicable_kinds = +dotnet_naming_symbols.test_methods.resharper_applicable_kinds = test_member +dotnet_naming_symbols.test_methods.resharper_required_modifiers = instance + +dotnet_naming_style.test_methods_style.capitalization = pascal_case +dotnet_naming_style.test_methods_style.word_separator = _ + +dotnet_naming_style.camel_case_style.capitalization = camel_case +dotnet_naming_style.pascal_case_style.capitalization = pascal_case + +# Microsoft .NET properties +csharp_preferred_modifier_order = public, protected, internal, private, new, abstract, virtual, override, sealed, static, readonly, extern, unsafe, volatile, async:suggestion +dotnet_sort_system_directives_first = true +dotnet_separate_import_directive_groups = false +dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion +dotnet_style_predefined_type_for_member_access = true:suggestion +dotnet_style_require_accessibility_modifiers = for_non_interface_members:suggestion + +# Parentheses settings +dotnet_style_parentheses_in_arithmetic_binary_operators = never_if_unnecessary:none +dotnet_style_parentheses_in_other_binary_operators = never_if_unnecessary:none +dotnet_style_parentheses_in_relational_binary_operators = never_if_unnecessary:none + +# Newline settings +csharp_new_line_before_open_brace = all +csharp_new_line_before_else = true +csharp_new_line_before_catch = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_object_initializers = false +csharp_new_line_before_members_in_anonymous_types = true +csharp_new_line_between_query_expression_clauses = true + +resharper_csharp_new_line_before_while = true + +# Indentation preferences +csharp_indent_block_contents = true +csharp_indent_braces = false +csharp_indent_case_contents = true +csharp_indent_case_contents_when_block = true +csharp_indent_switch_labels = true +csharp_indent_labels = flush_left + +# Prefer "var" everywhere +csharp_style_var_for_built_in_types = true:suggestion +csharp_style_var_when_type_is_apparent = true:suggestion +csharp_style_var_elsewhere = true:suggestion + +# Prefer method-like constructs to have a block body +csharp_style_expression_bodied_methods = when_on_single_line:suggestion +csharp_style_expression_bodied_constructors = when_on_single_line:suggestion +csharp_style_expression_bodied_operators = when_on_single_line:suggestion +csharp_style_expression_bodied_local_functions = when_on_single_line:suggestion + +resharper_csharp_keep_existing_expr_member_arrangement = true +resharper_csharp_place_expr_method_on_single_line = if_owner_is_single_line +resharper_csharp_place_expr_property_on_single_line = if_owner_is_single_line + +# Prefer property-like constructs to have an expression-body +csharp_style_expression_bodied_properties = true:suggestion +csharp_style_expression_bodied_indexers = true:suggestion +csharp_style_expression_bodied_accessors = true:suggestion + +resharper_local_function_body = expression_body +resharper_method_or_operator_body = expression_body + +# Suggest more modern language features when available +csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion +csharp_style_pattern_matching_over_as_with_null_check = true:suggestion +csharp_style_inlined_variable_declaration = true:suggestion +csharp_style_throw_expression = true:suggestion +csharp_style_conditional_delegate_call = true:suggestion +csharp_prefer_simple_default_expression = true:suggestion + +# Space preferences +csharp_space_after_cast = false +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_after_comma = true +csharp_space_after_dot = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_after_semicolon_in_for_statement = true +csharp_space_around_binary_operators = before_and_after +csharp_space_around_declaration_statements = false +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_before_comma = false +csharp_space_before_dot = false +csharp_space_before_open_square_brackets = false +csharp_space_before_semicolon_in_for_statement = false +csharp_space_between_empty_square_brackets = false +csharp_space_between_method_call_empty_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_method_declaration_name_and_open_parenthesis = false +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_parentheses = false +csharp_space_between_square_brackets = false + +# Blocks are allowed +csharp_prefer_braces = true:silent +csharp_preserve_single_line_blocks = true +csharp_preserve_single_line_statements = true +csharp_style_namespace_declarations = file_scoped + +resharper_csharp_braces_for_for = required +resharper_csharp_braces_for_foreach = required +resharper_csharp_braces_for_while = required +resharper_csharp_braces_for_ifelse = required_for_multiline_statement +resharper_csharp_space_within_single_line_array_initializer_braces = true + +# Alignment +resharper_csharp_align_linq_query = true +resharper_csharp_align_multiline_binary_expressions_chain = false +resharper_csharp_keep_blank_lines_in_code = 1 +resharper_csharp_keep_blank_lines_in_declarations = 1 +resharper_csharp_stick_comment = false +resharper_csharp_wrap_before_first_type_parameter_constraint = true +resharper_csharp_wrap_lines = false +resharper_csharp_wrap_multiple_type_parameter_constraints_style = chop_always +resharper_csharp_wrap_linq_expressions = chop_always +resharper_csharp_wrap_array_initializer_style = chop_if_long +resharper_csharp_place_constructor_initializer_on_same_line = false +resharper_csharp_place_accessorholder_attribute_on_same_line = false +resharper_csharp_place_field_attribute_on_same_line = false +resharper_csharp_place_simple_embedded_statement_on_same_line = false +resharper_wrap_before_arrow_with_expressions = true +resharper_csharp_wrap_before_arrow_with_expressions = true + +# Arrangement of method signatures +resharper_csharp_wrap_parameters_style = chop_if_long +resharper_csharp_max_formal_parameters_on_line = 3 +resharper_csharp_wrap_after_declaration_lpar = true + +# Arrangement of invocations +resharper_csharp_wrap_arguments_style = chop_if_long +resharper_csharp_max_invocation_arguments_on_line = 3 +resharper_csharp_wrap_after_invocation_lpar = true + +# Arguments +resharper_csharp_arguments_literal = named + +# Comments +resharper_xmldoc_indent_child_elements = RemoveIndent +resharper_xmldoc_indent_text = RemoveIndent +resharper_xmldoc_space_before_self_closing = false +resharper_xmldoc_max_line_length = 150 + +# Other +resharper_event_handler_pattern_long = $object$_On$event$ +resharper_empty_statement_highlighting = suggestion +resharper_csharp_trailing_comma_in_multiline_lists = true diff --git a/Directory.Packages.props b/Directory.Packages.props index bfee0ac..ff33eac 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -3,6 +3,6 @@ true - + - \ No newline at end of file + diff --git a/McQuery.Net.slnx b/McQuery.Net.slnx index 42b0c99..cb85641 100644 --- a/McQuery.Net.slnx +++ b/McQuery.Net.slnx @@ -1,3 +1,3 @@  - - \ No newline at end of file + + diff --git a/README.md b/README.md index f4f4a1f..6f9799d 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,9 @@ # McQuery.Net + Library for .Net which implements Minecraft Query protocol. You can use it for getting statuses of a Minecraft server. # Example of using + ```cs static async Task DoSomething(IEnumerable mcServersEndPoints) { diff --git a/sources/McQuery.Net/Data/Responses/BasicStatus.cs b/sources/McQuery.Net/Data/BasicStatus.cs similarity index 95% rename from sources/McQuery.Net/Data/Responses/BasicStatus.cs rename to sources/McQuery.Net/Data/BasicStatus.cs index 33d3ee5..89831ad 100644 --- a/sources/McQuery.Net/Data/Responses/BasicStatus.cs +++ b/sources/McQuery.Net/Data/BasicStatus.cs @@ -1,4 +1,4 @@ -namespace McQuery.Net.Data.Responses; +namespace McQuery.Net.Data; /// /// Represents data which is received from BasicStatus request. diff --git a/sources/McQuery.Net/Data/Responses/FullStatus.cs b/sources/McQuery.Net/Data/FullStatus.cs similarity index 96% rename from sources/McQuery.Net/Data/Responses/FullStatus.cs rename to sources/McQuery.Net/Data/FullStatus.cs index 961dfe4..d3ad8f2 100644 --- a/sources/McQuery.Net/Data/Responses/FullStatus.cs +++ b/sources/McQuery.Net/Data/FullStatus.cs @@ -1,4 +1,4 @@ -namespace McQuery.Net.Data.Responses; +namespace McQuery.Net.Data; /// /// Represents data which is received from FullStatus request. diff --git a/sources/McQuery.Net/Data/Parsers/HandshakeResponseParser.cs b/sources/McQuery.Net/Data/Parsers/HandshakeResponseParser.cs deleted file mode 100644 index de5b188..0000000 --- a/sources/McQuery.Net/Data/Parsers/HandshakeResponseParser.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System.Buffers; - -namespace McQuery.Net.Data.Parsers; - -internal class HandshakeResponseParser : ResponseParserBase, IResponseParser -{ - public override byte ResponseType => 0x09; - - public ChallengeToken Parse(byte[] data) - { - StartParsing(data, out SequenceReader reader); - - string challengeTokenString = ParseNullTerminatingString(ref reader); - byte[] challengeTokenBytes = BitConverter.GetBytes(int.Parse(challengeTokenString)); - - if (BitConverter.IsLittleEndian) - { - Array.Reverse(challengeTokenBytes); - } - - return new ChallengeToken(challengeTokenBytes); - } -} diff --git a/sources/McQuery.Net/Data/Parsers/StatusResponseParser.cs b/sources/McQuery.Net/Data/Parsers/StatusResponseParser.cs deleted file mode 100644 index 2c0de94..0000000 --- a/sources/McQuery.Net/Data/Parsers/StatusResponseParser.cs +++ /dev/null @@ -1,10 +0,0 @@ -using McQuery.Net.Data.Responses; - -namespace McQuery.Net.Data.Parsers; - -internal abstract class StatusResponseParser : ResponseParserBase, IResponseParser where T : StatusBase -{ - public override byte ResponseType => 0x00; - - public abstract T Parse(byte[] data); -} diff --git a/sources/McQuery.Net/Data/Responses/StatusBase.cs b/sources/McQuery.Net/Data/StatusBase.cs similarity index 78% rename from sources/McQuery.Net/Data/Responses/StatusBase.cs rename to sources/McQuery.Net/Data/StatusBase.cs index bf42700..3455608 100644 --- a/sources/McQuery.Net/Data/Responses/StatusBase.cs +++ b/sources/McQuery.Net/Data/StatusBase.cs @@ -1,4 +1,6 @@ -namespace McQuery.Net.Data.Responses; +using McQuery.Net.Internal.Data; + +namespace McQuery.Net.Data; [PublicAPI] public record StatusBase( diff --git a/sources/McQuery.Net/Exceptions/AlreadyExpiredException.cs b/sources/McQuery.Net/Exceptions/AlreadyExpiredException.cs index c020ab4..119fa56 100644 --- a/sources/McQuery.Net/Exceptions/AlreadyExpiredException.cs +++ b/sources/McQuery.Net/Exceptions/AlreadyExpiredException.cs @@ -1,11 +1,12 @@ -using McQuery.Net.Abstract; +using McQuery.Net.Internal.Abstract; namespace McQuery.Net.Exceptions; [PublicAPI] public class AlreadyExpiredException : ArgumentException { - internal AlreadyExpiredException(IExpirable expirable) : base($"{expirable.GetType().Name} is already expired") + internal AlreadyExpiredException(IExpirable expirable) + : base($"{expirable.GetType().Name} is already expired") { } diff --git a/sources/McQuery.Net/IMcQueryClient.cs b/sources/McQuery.Net/IMcQueryClient.cs index f2535b2..0072646 100644 --- a/sources/McQuery.Net/IMcQueryClient.cs +++ b/sources/McQuery.Net/IMcQueryClient.cs @@ -1,5 +1,5 @@ using System.Net; -using McQuery.Net.Data.Responses; +using McQuery.Net.Data; namespace McQuery.Net; diff --git a/sources/McQuery.Net/IMcQueryClientFactory.cs b/sources/McQuery.Net/IMcQueryClientFactory.cs index ecb3b24..3e53f3d 100644 --- a/sources/McQuery.Net/IMcQueryClientFactory.cs +++ b/sources/McQuery.Net/IMcQueryClientFactory.cs @@ -1,5 +1,6 @@ namespace McQuery.Net; +[PublicAPI] public interface IMcQueryClientFactory { IMcQueryClient Get(); diff --git a/sources/McQuery.Net/Abstract/IAuthOnlyClient.cs b/sources/McQuery.Net/Internal/Abstract/IAuthOnlyClient.cs similarity index 82% rename from sources/McQuery.Net/Abstract/IAuthOnlyClient.cs rename to sources/McQuery.Net/Internal/Abstract/IAuthOnlyClient.cs index 58f5612..a3eed6a 100644 --- a/sources/McQuery.Net/Abstract/IAuthOnlyClient.cs +++ b/sources/McQuery.Net/Internal/Abstract/IAuthOnlyClient.cs @@ -1,7 +1,7 @@ using System.Net; -using McQuery.Net.Data; +using McQuery.Net.Internal.Data; -namespace McQuery.Net.Abstract; +namespace McQuery.Net.Internal.Abstract; /// /// Client that provides interface to acquire . @@ -18,5 +18,6 @@ internal interface IAuthOnlyClient : IDisposable internal Task HandshakeAsync( IPEndPoint serverEndpoint, SessionId sessionId, - CancellationToken cancellationToken = default); + CancellationToken cancellationToken = default + ); } diff --git a/sources/McQuery.Net/Abstract/IExpoirable.cs b/sources/McQuery.Net/Internal/Abstract/IExpoirable.cs similarity index 61% rename from sources/McQuery.Net/Abstract/IExpoirable.cs rename to sources/McQuery.Net/Internal/Abstract/IExpoirable.cs index 8211b08..68e3e2f 100644 --- a/sources/McQuery.Net/Abstract/IExpoirable.cs +++ b/sources/McQuery.Net/Internal/Abstract/IExpoirable.cs @@ -1,6 +1,6 @@ -namespace McQuery.Net.Abstract; +namespace McQuery.Net.Internal.Abstract; internal interface IExpirable { public bool IsExpired { get; } -} \ No newline at end of file +} diff --git a/sources/McQuery.Net/Data/ChallengeToken.cs b/sources/McQuery.Net/Internal/Data/ChallengeToken.cs similarity index 77% rename from sources/McQuery.Net/Data/ChallengeToken.cs rename to sources/McQuery.Net/Internal/Data/ChallengeToken.cs index 3d542bc..8008cdb 100644 --- a/sources/McQuery.Net/Data/ChallengeToken.cs +++ b/sources/McQuery.Net/Internal/Data/ChallengeToken.cs @@ -1,7 +1,7 @@ -using McQuery.Net.Abstract; using McQuery.Net.Exceptions; +using McQuery.Net.Internal.Abstract; -namespace McQuery.Net.Data; +namespace McQuery.Net.Internal.Data; /// /// Secret value provided by Minecraft server to issue status requests. @@ -9,7 +9,7 @@ namespace McQuery.Net.Data; internal record ChallengeToken : IExpirable { private const int AlivePeriod = 29; - private readonly DateTime expiresAt = DateTime.UtcNow.AddSeconds(AlivePeriod); + private readonly DateTime _expiresAt = DateTime.UtcNow.AddSeconds(AlivePeriod); /// /// .ctor. @@ -21,13 +21,15 @@ internal record ChallengeToken : IExpirable public ChallengeToken(byte[] data) { if (data.Length != 4) + { throw new ArgumentOutOfRangeException(nameof(data), data, "Challenge token must have 4 bytes"); + } Data = data; } private byte[] Data { get; } - public bool IsExpired => DateTime.UtcNow >= expiresAt; + public bool IsExpired => DateTime.UtcNow >= _expiresAt; public static implicit operator byte[](ChallengeToken token) { @@ -35,6 +37,6 @@ public static implicit operator byte[](ChallengeToken token) return [..token.Data]; } - public static implicit operator ReadOnlySpan(ChallengeToken token) => - (byte[])token; + public static implicit operator ReadOnlySpan(ChallengeToken token) + => (byte[])token; } diff --git a/sources/McQuery.Net/Data/Session.cs b/sources/McQuery.Net/Internal/Data/Session.cs similarity index 92% rename from sources/McQuery.Net/Data/Session.cs rename to sources/McQuery.Net/Internal/Data/Session.cs index 4351de9..2933bf0 100644 --- a/sources/McQuery.Net/Data/Session.cs +++ b/sources/McQuery.Net/Internal/Data/Session.cs @@ -1,4 +1,4 @@ -namespace McQuery.Net.Data; +namespace McQuery.Net.Internal.Data; /// /// Represents a combination of and values. diff --git a/sources/McQuery.Net/Data/SessionId.cs b/sources/McQuery.Net/Internal/Data/SessionId.cs similarity index 76% rename from sources/McQuery.Net/Data/SessionId.cs rename to sources/McQuery.Net/Internal/Data/SessionId.cs index 6b96d33..f6e46aa 100644 --- a/sources/McQuery.Net/Data/SessionId.cs +++ b/sources/McQuery.Net/Internal/Data/SessionId.cs @@ -1,4 +1,4 @@ -namespace McQuery.Net.Data; +namespace McQuery.Net.Internal.Data; /// /// Represents Session Identifier. @@ -24,24 +24,24 @@ internal class SessionId public SessionId(byte[] data) { if (data.Length != 4) + { throw new ArgumentOutOfRangeException(nameof(data), data, "Session identifier must have 4 bytes"); + } Data = data; } - public static implicit operator string(SessionId sessionId) => - BitConverter.ToString(sessionId.Data); + public static implicit operator string(SessionId sessionId) => BitConverter.ToString(sessionId.Data); - public static implicit operator byte[](SessionId sessionId) => - [..sessionId.Data]; + public static implicit operator byte[](SessionId sessionId) => [..sessionId.Data]; - public static implicit operator ReadOnlySpan(SessionId sessionId) => - (byte[])sessionId; + public static implicit operator ReadOnlySpan(SessionId sessionId) => (byte[])sessionId; - public override bool Equals(object? obj) => - obj is SessionId anotherSessionId - && Data.SequenceEqual(anotherSessionId.Data); + public override bool Equals(object? obj) + { + return obj is SessionId anotherSessionId + && Data.SequenceEqual(anotherSessionId.Data); + } - public override int GetHashCode() => - BitConverter.ToInt32(Data, 0); + public override int GetHashCode() => BitConverter.ToInt32(Data, startIndex: 0); } diff --git a/sources/McQuery.Net/Data/Factories/IRequestFactory.cs b/sources/McQuery.Net/Internal/Factories/IRequestFactory.cs similarity index 92% rename from sources/McQuery.Net/Data/Factories/IRequestFactory.cs rename to sources/McQuery.Net/Internal/Factories/IRequestFactory.cs index 9509018..087f5f3 100644 --- a/sources/McQuery.Net/Data/Factories/IRequestFactory.cs +++ b/sources/McQuery.Net/Internal/Factories/IRequestFactory.cs @@ -1,4 +1,6 @@ -namespace McQuery.Net.Data.Factories; +using McQuery.Net.Internal.Data; + +namespace McQuery.Net.Internal.Factories; /// /// Provides methods to build requests. diff --git a/sources/McQuery.Net/Data/Factories/RequestFactory.cs b/sources/McQuery.Net/Internal/Factories/RequestFactory.cs similarity index 89% rename from sources/McQuery.Net/Data/Factories/RequestFactory.cs rename to sources/McQuery.Net/Internal/Factories/RequestFactory.cs index 6094f41..828756e 100644 --- a/sources/McQuery.Net/Data/Factories/RequestFactory.cs +++ b/sources/McQuery.Net/Internal/Factories/RequestFactory.cs @@ -1,4 +1,6 @@ -namespace McQuery.Net.Data.Factories; +using McQuery.Net.Internal.Data; + +namespace McQuery.Net.Internal.Factories; /// /// Implementation of . @@ -7,7 +9,7 @@ internal class RequestFactory : IRequestFactory { private const byte HandshakeRequestTypeConst = 0x09; private const byte StatusRequestTypeConst = 0x00; - private static readonly byte[] MagicConst = [0xfe, 0xfd]; + private static readonly byte[] magicConst = [0xfe, 0xfd]; /// public byte[] GetHandshakeRequest(SessionId sessionId) @@ -36,7 +38,7 @@ public byte[] GetFullStatusRequest(Session session) private static void FormRequestHeader(Stream packetStream, byte packageType, SessionId sessionId) { - packetStream.Write(MagicConst); + packetStream.Write(magicConst); packetStream.Write([packageType]); packetStream.Write(sessionId); } diff --git a/sources/McQuery.Net/Data/Parsers/BasicStatusResponseParser.cs b/sources/McQuery.Net/Internal/Parsers/BasicStatusResponseParser.cs similarity index 73% rename from sources/McQuery.Net/Data/Parsers/BasicStatusResponseParser.cs rename to sources/McQuery.Net/Internal/Parsers/BasicStatusResponseParser.cs index 2d3de9b..4543d93 100644 --- a/sources/McQuery.Net/Data/Parsers/BasicStatusResponseParser.cs +++ b/sources/McQuery.Net/Internal/Parsers/BasicStatusResponseParser.cs @@ -1,13 +1,12 @@ -using System.Buffers; -using McQuery.Net.Data.Responses; +using McQuery.Net.Data; -namespace McQuery.Net.Data.Parsers; +namespace McQuery.Net.Internal.Parsers; internal class BasicStatusResponseParser : StatusResponseParser { public override BasicStatus Parse(byte[] data) { - SessionId sessionId = StartParsing(data, out SequenceReader reader); + var sessionId = StartParsing(data, out var reader); return new BasicStatus( ParseNullTerminatingString(ref reader), @@ -19,7 +18,7 @@ public override BasicStatus Parse(byte[] data) ParseNullTerminatingString(ref reader) ) { - SessionId = sessionId + SessionId = sessionId, }; } } diff --git a/sources/McQuery.Net/Data/Parsers/FullStatusResponseParser.cs b/sources/McQuery.Net/Internal/Parsers/FullStatusResponseParser.cs similarity index 58% rename from sources/McQuery.Net/Data/Parsers/FullStatusResponseParser.cs rename to sources/McQuery.Net/Internal/Parsers/FullStatusResponseParser.cs index bc154b5..f507467 100644 --- a/sources/McQuery.Net/Data/Parsers/FullStatusResponseParser.cs +++ b/sources/McQuery.Net/Internal/Parsers/FullStatusResponseParser.cs @@ -1,40 +1,31 @@ -using System.Buffers; -using McQuery.Net.Data.Responses; +using McQuery.Net.Data; -namespace McQuery.Net.Data.Parsers; +namespace McQuery.Net.Internal.Parsers; internal class FullStatusResponseParser : StatusResponseParser { - private static readonly byte[] Constant1 = [0x73, 0x70, 0x6C, 0x69, 0x74, 0x6E, 0x75, 0x6D, 0x00, 0x80, 0x00]; - - private static readonly byte[] Constant2 = [0x01, 0x70, 0x6C, 0x61, 0x79, 0x65, 0x72, 0x5F, 0x00, 0x00]; - - private static readonly InvalidOperationException ResponseFormatError = new("Invalid full status response format"); + private static readonly byte[] constant1 = [0x73, 0x70, 0x6C, 0x69, 0x74, 0x6E, 0x75, 0x6D, 0x00, 0x80, 0x00]; + private static readonly byte[] constant2 = [0x01, 0x70, 0x6C, 0x61, 0x79, 0x65, 0x72, 0x5F, 0x00, 0x00]; + private static readonly InvalidOperationException responseFormatError = new("Invalid full status response format"); public override FullStatus Parse(byte[] data) { - SessionId sessionId = StartParsing(data, out SequenceReader reader); + var sessionId = StartParsing(data, out var reader); - if (!reader.IsNext(Constant1, true)) - { - throw ResponseFormatError; - } + if (!reader.IsNext(constant1, advancePast: true)) throw responseFormatError; Dictionary statusKeyValues = new(); - while (!reader.IsNext(0, true)) + while (!reader.IsNext(next: 0, advancePast: true)) { - string key = ParseNullTerminatingString(ref reader); - string value = ParseNullTerminatingString(ref reader); + var key = ParseNullTerminatingString(ref reader); + var value = ParseNullTerminatingString(ref reader); statusKeyValues.Add(key, value); } - if (!reader.IsNext(Constant2, true)) - { - throw ResponseFormatError; - } + if (!reader.IsNext(constant2, advancePast: true)) throw responseFormatError; List players = []; - while (!reader.IsNext(0, true)) + while (!reader.IsNext(next: 0, advancePast: true)) { players.Add(ParseNullTerminatingString(ref reader)); } diff --git a/sources/McQuery.Net/Internal/Parsers/HandshakeResponseParser.cs b/sources/McQuery.Net/Internal/Parsers/HandshakeResponseParser.cs new file mode 100644 index 0000000..dab59fb --- /dev/null +++ b/sources/McQuery.Net/Internal/Parsers/HandshakeResponseParser.cs @@ -0,0 +1,20 @@ +using McQuery.Net.Internal.Data; + +namespace McQuery.Net.Internal.Parsers; + +internal class HandshakeResponseParser : ResponseParserBase, IResponseParser +{ + protected override byte ResponseType => 0x09; + + public ChallengeToken Parse(byte[] data) + { + StartParsing(data, out var reader); + + var challengeTokenString = ParseNullTerminatingString(ref reader); + var challengeTokenBytes = BitConverter.GetBytes(int.Parse(challengeTokenString)); + + if (BitConverter.IsLittleEndian) Array.Reverse(challengeTokenBytes); + + return new ChallengeToken(challengeTokenBytes); + } +} diff --git a/sources/McQuery.Net/Data/Parsers/IResponseParser.cs b/sources/McQuery.Net/Internal/Parsers/IResponseParser.cs similarity index 64% rename from sources/McQuery.Net/Data/Parsers/IResponseParser.cs rename to sources/McQuery.Net/Internal/Parsers/IResponseParser.cs index 3f9d145..a702317 100644 --- a/sources/McQuery.Net/Data/Parsers/IResponseParser.cs +++ b/sources/McQuery.Net/Internal/Parsers/IResponseParser.cs @@ -1,4 +1,4 @@ -namespace McQuery.Net.Data.Parsers; +namespace McQuery.Net.Internal.Parsers; internal interface IResponseParser { diff --git a/sources/McQuery.Net/Data/Parsers/ResponseParserBase.cs b/sources/McQuery.Net/Internal/Parsers/ResponseParserBase.cs similarity index 71% rename from sources/McQuery.Net/Data/Parsers/ResponseParserBase.cs rename to sources/McQuery.Net/Internal/Parsers/ResponseParserBase.cs index c50c6cc..ab3bbbc 100644 --- a/sources/McQuery.Net/Data/Parsers/ResponseParserBase.cs +++ b/sources/McQuery.Net/Internal/Parsers/ResponseParserBase.cs @@ -1,21 +1,19 @@ using System.Buffers; using System.Text; +using McQuery.Net.Internal.Data; -namespace McQuery.Net.Data.Parsers; +namespace McQuery.Net.Internal.Parsers; internal abstract class ResponseParserBase { - public abstract byte ResponseType { get; } + protected abstract byte ResponseType { get; } - public SessionId StartParsing(byte[] data, out SequenceReader reader) + internal SessionId StartParsing(byte[] data, out SequenceReader reader) { ReadOnlySequence sequence = new(data); reader = new SequenceReader(sequence); - if (!reader.IsNext([ResponseType], true)) - { - throw new InvalidOperationException("Invalid response type"); - } + if (!reader.IsNext([ResponseType], advancePast: true)) throw new InvalidOperationException("Invalid response type"); return ParseSessionId(ref reader); } @@ -27,15 +25,17 @@ private static SessionId ParseSessionId(ref SequenceReader reader) throw new InvalidOperationException("Session id must contain exactly 4 bytes."); } - reader.TryReadExact(4, out ReadOnlySequence sessionIdBytes); + reader.TryReadExact(count: 4, out var sessionIdBytes); return new SessionId(sessionIdBytes.ToArray()); } internal static string ParseNullTerminatingString(ref SequenceReader reader) { - if (!reader.TryReadTo(out ReadOnlySequence bytes, 0, true)) + if (!reader.TryReadTo(out ReadOnlySequence bytes, delimiter: 0, advancePastDelimiter: true)) + { throw new InvalidOperationException("Cannot parse null terminating string: terminator was not found."); + } return Encoding.ASCII.GetString(bytes); } @@ -43,7 +43,9 @@ internal static string ParseNullTerminatingString(ref SequenceReader reade internal static short ParseShortLittleEndian(ref SequenceReader reader) { if (!reader.TryReadLittleEndian(out short port)) + { throw new InvalidOperationException("Cannot parse short value"); + } return port; } diff --git a/sources/McQuery.Net/Internal/Parsers/StatusResponseParser.cs b/sources/McQuery.Net/Internal/Parsers/StatusResponseParser.cs new file mode 100644 index 0000000..2457aea --- /dev/null +++ b/sources/McQuery.Net/Internal/Parsers/StatusResponseParser.cs @@ -0,0 +1,11 @@ +using McQuery.Net.Data; + +namespace McQuery.Net.Internal.Parsers; + +internal abstract class StatusResponseParser : ResponseParserBase, IResponseParser + where T : StatusBase +{ + protected override byte ResponseType => 0x00; + + public abstract T Parse(byte[] data); +} diff --git a/sources/McQuery.Net/Data/Providers/ISessionIdProvider.cs b/sources/McQuery.Net/Internal/Providers/ISessionIdProvider.cs similarity index 78% rename from sources/McQuery.Net/Data/Providers/ISessionIdProvider.cs rename to sources/McQuery.Net/Internal/Providers/ISessionIdProvider.cs index a8b271f..6dbb297 100644 --- a/sources/McQuery.Net/Data/Providers/ISessionIdProvider.cs +++ b/sources/McQuery.Net/Internal/Providers/ISessionIdProvider.cs @@ -1,4 +1,6 @@ -namespace McQuery.Net.Data.Providers; +using McQuery.Net.Internal.Data; + +namespace McQuery.Net.Internal.Providers; /// /// Provides every time it's needed. diff --git a/sources/McQuery.Net/Data/Providers/ISessionStorage.cs b/sources/McQuery.Net/Internal/Providers/ISessionStorage.cs similarity index 80% rename from sources/McQuery.Net/Data/Providers/ISessionStorage.cs rename to sources/McQuery.Net/Internal/Providers/ISessionStorage.cs index 3bba385..c9f9b99 100644 --- a/sources/McQuery.Net/Data/Providers/ISessionStorage.cs +++ b/sources/McQuery.Net/Internal/Providers/ISessionStorage.cs @@ -1,11 +1,12 @@ using System.Net; +using McQuery.Net.Internal.Data; -namespace McQuery.Net.Data.Providers; +namespace McQuery.Net.Internal.Providers; /// /// Creates and stores objects. /// -internal interface ISessionStorage: IDisposable +internal interface ISessionStorage : IDisposable { /// /// Gets stored session or acquire new. diff --git a/sources/McQuery.Net/Data/Providers/SessionIdProvider.cs b/sources/McQuery.Net/Internal/Providers/SessionIdProvider.cs similarity index 51% rename from sources/McQuery.Net/Data/Providers/SessionIdProvider.cs rename to sources/McQuery.Net/Internal/Providers/SessionIdProvider.cs index 0f7ac6e..b5587db 100644 --- a/sources/McQuery.Net/Data/Providers/SessionIdProvider.cs +++ b/sources/McQuery.Net/Internal/Providers/SessionIdProvider.cs @@ -1,4 +1,6 @@ -namespace McQuery.Net.Data.Providers; +using McQuery.Net.Internal.Data; + +namespace McQuery.Net.Internal.Providers; /// /// Implementation of . @@ -10,13 +12,10 @@ internal class SessionIdProvider : ISessionIdProvider /// public SessionId Get() { - uint currentValue = Interlocked.Increment(ref counter); + var currentValue = Interlocked.Increment(ref counter); - byte[] bytes = BitConverter.GetBytes(currentValue); - if (BitConverter.IsLittleEndian) - { - Array.Reverse(bytes); - } + var bytes = BitConverter.GetBytes(currentValue); + if (BitConverter.IsLittleEndian) Array.Reverse(bytes); return new SessionId(bytes); } diff --git a/sources/McQuery.Net/Data/Providers/SessionStorage.cs b/sources/McQuery.Net/Internal/Providers/SessionStorage.cs similarity index 60% rename from sources/McQuery.Net/Data/Providers/SessionStorage.cs rename to sources/McQuery.Net/Internal/Providers/SessionStorage.cs index b6f9972..f07de02 100644 --- a/sources/McQuery.Net/Data/Providers/SessionStorage.cs +++ b/sources/McQuery.Net/Internal/Providers/SessionStorage.cs @@ -1,8 +1,9 @@ using System.Collections.Concurrent; using System.Net; -using McQuery.Net.Abstract; +using McQuery.Net.Internal.Abstract; +using McQuery.Net.Internal.Data; -namespace McQuery.Net.Data.Providers; +namespace McQuery.Net.Internal.Providers; /// /// Implementation of . @@ -10,13 +11,13 @@ namespace McQuery.Net.Data.Providers; /// . internal class SessionStorage(ISessionIdProvider sessionIdProvider) : ISessionStorage { - private IAuthOnlyClient? authClient; - private readonly ConcurrentDictionary sessionsByEndpoints = new(); + private IAuthOnlyClient? _authClient; + private readonly ConcurrentDictionary _sessionsByEndpoints = new(); /// public async Task GetAsync(IPEndPoint serverEndpoint, CancellationToken cancellationToken = default) { - bool sessionExists = sessionsByEndpoints.TryGetValue(serverEndpoint, out Session? session); + var sessionExists = _sessionsByEndpoints.TryGetValue(serverEndpoint, out var session); if (sessionExists && !session!.IsExpired) { @@ -25,7 +26,7 @@ public async Task GetAsync(IPEndPoint serverEndpoint, CancellationToken if (sessionExists && session!.IsExpired) { - if (!sessionsByEndpoints.TryRemove(serverEndpoint, out _)) + if (!_sessionsByEndpoints.TryRemove(serverEndpoint, out _)) { throw new Exception($"Cannot remove expired session {session} for some reason."); } @@ -36,26 +37,23 @@ public async Task GetAsync(IPEndPoint serverEndpoint, CancellationToken internal void Init(IAuthOnlyClient client) { - if (authClient != null) - { - throw new InvalidOperationException("SessionStorage already initialized."); - } + if (_authClient != null) throw new InvalidOperationException("SessionStorage already initialized."); - authClient = client; + _authClient = client; } private async Task AcquireSession(IPEndPoint serverEndpoint, CancellationToken cancellationToken) { - if (authClient == null) + if (_authClient == null) { throw new InvalidOperationException("Storage must be initialized before calling this method."); } - SessionId sessionId = sessionIdProvider.Get(); - ChallengeToken challengeToken = await authClient!.HandshakeAsync(serverEndpoint, sessionId, cancellationToken); + var sessionId = sessionIdProvider.Get(); + var challengeToken = await _authClient!.HandshakeAsync(serverEndpoint, sessionId, cancellationToken); Session session = new(sessionId, challengeToken); - if (!sessionsByEndpoints.TryAdd(serverEndpoint, session)) + if (!_sessionsByEndpoints.TryAdd(serverEndpoint, session)) { throw new Exception("Cannot add session for endpoint " + serverEndpoint + " for some reason."); } @@ -63,13 +61,14 @@ private async Task AcquireSession(IPEndPoint serverEndpoint, Cancellati return session; } - private bool isDisposed; + private bool _isDisposed; + public void Dispose() { - if(isDisposed) return; + if (_isDisposed) return; - authClient?.Dispose(); + _authClient?.Dispose(); GC.SuppressFinalize(this); - isDisposed = true; + _isDisposed = true; } } diff --git a/sources/McQuery.Net/McQuery.Net.csproj b/sources/McQuery.Net/McQuery.Net.csproj index 3196a44..2f15a9e 100644 --- a/sources/McQuery.Net/McQuery.Net.csproj +++ b/sources/McQuery.Net/McQuery.Net.csproj @@ -1,15 +1,15 @@  - - true - - - - - - - - - + + true + + + + + + + + + diff --git a/sources/McQuery.Net/McQueryClient.cs b/sources/McQuery.Net/McQueryClient.cs index 1c5c134..4d97c2f 100644 --- a/sources/McQuery.Net/McQueryClient.cs +++ b/sources/McQuery.Net/McQueryClient.cs @@ -1,51 +1,55 @@ using System.Net; using System.Net.Sockets; -using McQuery.Net.Abstract; using McQuery.Net.Data; -using McQuery.Net.Data.Factories; -using McQuery.Net.Data.Parsers; -using McQuery.Net.Data.Providers; -using McQuery.Net.Data.Responses; +using McQuery.Net.Internal.Abstract; +using McQuery.Net.Internal.Data; +using McQuery.Net.Internal.Factories; +using McQuery.Net.Internal.Parsers; +using McQuery.Net.Internal.Providers; namespace McQuery.Net; /// /// Implementation of . /// -[PublicAPI] +[UsedImplicitly] public class McQueryClient : IMcQueryClient, IAuthOnlyClient { - private readonly UdpClient socket; - private readonly IRequestFactory requestFactory; - private readonly ISessionStorage sessionStorage; - private int responseTimeoutSeconds = 5; // TODO + private readonly UdpClient _socket; + private readonly IRequestFactory _requestFactory; + private readonly ISessionStorage _sessionStorage; + private int _responseTimeoutSeconds = 5; // TODO - private static readonly IResponseParser HandshakeResponseParser = new HandshakeResponseParser(); - private static readonly IResponseParser BasicStatusResponseParser = new BasicStatusResponseParser(); - private static readonly IResponseParser FullStatusResponseParser = new FullStatusResponseParser(); + private static readonly IResponseParser handshakeResponseParser = new HandshakeResponseParser(); + private static readonly IResponseParser basicStatusResponseParser = new BasicStatusResponseParser(); + private static readonly IResponseParser fullStatusResponseParser = new FullStatusResponseParser(); internal McQueryClient(UdpClient socket, IRequestFactory requestFactory, ISessionStorage sessionStorage) { - this.requestFactory = requestFactory; - this.sessionStorage = sessionStorage; - this.socket = socket; + _requestFactory = requestFactory; + _sessionStorage = sessionStorage; + _socket = socket; } /// - public async Task GetBasicStatusAsync(IPEndPoint serverEndpoint, CancellationToken cancellationToken) => - await SendRequestAsync( + public async Task GetBasicStatusAsync(IPEndPoint serverEndpoint, CancellationToken cancellationToken) + { + return await SendRequestAsync( serverEndpoint, - session => requestFactory.GetBasicStatusRequest(session), - BasicStatusResponseParser, + session => _requestFactory.GetBasicStatusRequest(session), + basicStatusResponseParser, cancellationToken); + } /// - public async Task GetFullStatusAsync(IPEndPoint serverEndpoint, CancellationToken cancellationToken) => - await SendRequestAsync( + public async Task GetFullStatusAsync(IPEndPoint serverEndpoint, CancellationToken cancellationToken) + { + return await SendRequestAsync( serverEndpoint, - session => requestFactory.GetFullStatusRequest(session), - FullStatusResponseParser, + session => _requestFactory.GetFullStatusRequest(session), + fullStatusResponseParser, cancellationToken); + } /// public BasicStatus GetBasicStatus(IPEndPoint serverEndpoint, CancellationToken cancellationToken) => @@ -59,14 +63,15 @@ public FullStatus GetFullStatus(IPEndPoint serverEndpoint, CancellationToken can async Task IAuthOnlyClient.HandshakeAsync( IPEndPoint serverEndpoint, SessionId sessionId, - CancellationToken cancellationToken) + CancellationToken cancellationToken + ) { - byte[] packet = requestFactory.GetHandshakeRequest(sessionId); + var packet = _requestFactory.GetHandshakeRequest(sessionId); return await SendRequestAsync( serverEndpoint, packet, - HandshakeResponseParser, + handshakeResponseParser, cancellationToken); } @@ -74,47 +79,54 @@ private async Task SendRequestAsync( IPEndPoint serverEndpoint, Func> packetFactory, IResponseParser responseParser, - CancellationToken cancellationToken = default) + CancellationToken cancellationToken = default + ) { - Session session = await sessionStorage.GetAsync(serverEndpoint, cancellationToken); - ReadOnlyMemory packet = packetFactory(session); - return await SendRequestAsync(serverEndpoint, packet, responseParser, cancellationToken); + var session = await _sessionStorage.GetAsync(serverEndpoint, cancellationToken); + var packet = packetFactory(session); + return await SendRequestAsync( + serverEndpoint, + packet, + responseParser, + cancellationToken); } private async Task SendRequestAsync( IPEndPoint serverEndpoint, ReadOnlyMemory packet, IResponseParser responseParser, - CancellationToken cancellationToken = default) + CancellationToken cancellationToken = default + ) { - Console.WriteLine($"Sending {packet.Length} bytes to {serverEndpoint} with content {BitConverter.ToString(packet.ToArray())}"); + Console.WriteLine( + $"Sending {packet.Length} bytes to {serverEndpoint} with content {BitConverter.ToString(packet.ToArray())}"); - await socket.SendAsync(packet, serverEndpoint, cancellationToken).ConfigureAwait(false); + await _socket.SendAsync(packet, serverEndpoint, cancellationToken).ConfigureAwait(continueOnCapturedContext: false); - using CancellationTokenSource timeoutSource = new(TimeSpan.FromSeconds(responseTimeoutSeconds)); - using CancellationTokenSource linkedSource = + using CancellationTokenSource timeoutSource = new(TimeSpan.FromSeconds(_responseTimeoutSeconds)); + using var linkedSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutSource.Token); - CancellationToken tokenWithTimeout = linkedSource.Token; + var tokenWithTimeout = linkedSource.Token; // TODO: common response pool - UdpReceiveResult response = await socket.ReceiveAsync(tokenWithTimeout).ConfigureAwait(false); + var response = await _socket.ReceiveAsync(tokenWithTimeout).ConfigureAwait(continueOnCapturedContext: false); Console.WriteLine($"Received response from server: {BitConverter.ToString(response.Buffer)}"); - T responseData = responseParser.Parse(response.Buffer); + var responseData = responseParser.Parse(response.Buffer); Console.WriteLine(responseData); return responseData; } + private bool _isDisposed; - private bool isDisposed; public void Dispose() { - if(isDisposed) return; + if (_isDisposed) return; - socket.Dispose(); - sessionStorage.Dispose(); + _socket.Dispose(); + _sessionStorage.Dispose(); GC.SuppressFinalize(this); - isDisposed = true; + _isDisposed = true; } } diff --git a/sources/McQuery.Net/McQueryClientFactory.cs b/sources/McQuery.Net/McQueryClientFactory.cs index caa6ba3..7e6aacb 100644 --- a/sources/McQuery.Net/McQueryClientFactory.cs +++ b/sources/McQuery.Net/McQueryClientFactory.cs @@ -1,16 +1,17 @@ using System.Net.Sockets; -using McQuery.Net.Data.Factories; -using McQuery.Net.Data.Providers; +using McQuery.Net.Internal.Factories; +using McQuery.Net.Internal.Providers; namespace McQuery.Net; +[UsedImplicitly] public class McQueryClientFactory : IMcQueryClientFactory { - private readonly Lazy sessionIdProvider = new(() => new SessionIdProvider(), isThreadSafe: true); + private readonly Lazy _sessionIdProvider = new(() => new SessionIdProvider(), isThreadSafe: true); public IMcQueryClient Get() { - SessionStorage sessionStorage = new(sessionIdProvider.Value); + SessionStorage sessionStorage = new(_sessionIdProvider.Value); McQueryClient client = new( new UdpClient(), From ed4c759133f675fd85d7d7000a3ae8ee8247edc6 Mon Sep 17 00:00:00 2001 From: Maxim Liven Date: Tue, 19 Nov 2024 14:12:40 +0300 Subject: [PATCH 11/31] feat: implement concurrency --- Directory.Packages.props | 8 +- sources/McQuery.Net/IMcQueryClient.cs | 6 -- .../CancellationTokenTimeoutEnricher.cs | 43 +++++++++++ .../Internal/Providers/SessionStorage.cs | 31 +++++--- sources/McQuery.Net/McQuery.Net.csproj | 6 ++ sources/McQuery.Net/McQueryClient.cs | 76 +++++++++++-------- sources/McQuery.Net/McQueryClientFactory.cs | 21 ++++- 7 files changed, 138 insertions(+), 53 deletions(-) create mode 100644 sources/McQuery.Net/Internal/Helpers/CancellationTokenTimeoutEnricher.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index ff33eac..c226878 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -3,6 +3,10 @@ true - + + + + + - + \ No newline at end of file diff --git a/sources/McQuery.Net/IMcQueryClient.cs b/sources/McQuery.Net/IMcQueryClient.cs index 0072646..62d3f53 100644 --- a/sources/McQuery.Net/IMcQueryClient.cs +++ b/sources/McQuery.Net/IMcQueryClient.cs @@ -17,9 +17,6 @@ public interface IMcQueryClient : IDisposable /// . Task GetBasicStatusAsync(IPEndPoint serverEndpoint, CancellationToken cancellationToken = default); - /// - BasicStatus GetBasicStatus(IPEndPoint serverEndpoint, CancellationToken cancellationToken = default); - /// /// Get . /// @@ -30,7 +27,4 @@ public interface IMcQueryClient : IDisposable /// . /// . Task GetFullStatusAsync(IPEndPoint serverEndpoint, CancellationToken cancellationToken = default); - - /// - FullStatus GetFullStatus(IPEndPoint serverEndpoint, CancellationToken cancellationToken = default); } diff --git a/sources/McQuery.Net/Internal/Helpers/CancellationTokenTimeoutEnricher.cs b/sources/McQuery.Net/Internal/Helpers/CancellationTokenTimeoutEnricher.cs new file mode 100644 index 0000000..ae71c8e --- /dev/null +++ b/sources/McQuery.Net/Internal/Helpers/CancellationTokenTimeoutEnricher.cs @@ -0,0 +1,43 @@ +using System.Diagnostics.CodeAnalysis; + +namespace McQuery.Net.Internal.Helpers; + +public static class CancellationTokenTimeoutEnrichHelper +{ + public static CancellationTokenSourceWithTimeout ToSourceWithTimeout(this CancellationToken token, TimeSpan timeout) + => CancellationTokenSourceWithTimeout.Create(token, timeout); + + public record struct CancellationTokenSourceWithTimeout : IDisposable + { + private readonly CancellationTokenSource _originSource; + private readonly CancellationTokenSource _linkedSource; + + public CancellationToken Token => _linkedSource.Token; + + private CancellationTokenSourceWithTimeout(CancellationTokenSource originSource, CancellationTokenSource linkedSource) + { + _originSource = originSource; + _linkedSource = linkedSource; + } + + [SuppressMessage("Design", "CA1068:CancellationToken parameters must come last")] + public static CancellationTokenSourceWithTimeout Create(CancellationToken cancellationToken, TimeSpan timeout) + { + CancellationTokenSource timeoutSource = new(timeout); + return new CancellationTokenSourceWithTimeout( + timeoutSource, + CancellationTokenSource.CreateLinkedTokenSource( + cancellationToken, + timeoutSource.Token)); + } + + private bool _isDisposed = false; + public void Dispose() + { + if (_isDisposed) return; + _originSource.Dispose(); + _linkedSource.Dispose(); + _isDisposed = true; + } + } +} diff --git a/sources/McQuery.Net/Internal/Providers/SessionStorage.cs b/sources/McQuery.Net/Internal/Providers/SessionStorage.cs index f07de02..4b5bff6 100644 --- a/sources/McQuery.Net/Internal/Providers/SessionStorage.cs +++ b/sources/McQuery.Net/Internal/Providers/SessionStorage.cs @@ -1,7 +1,8 @@ -using System.Collections.Concurrent; +using System.Diagnostics.CodeAnalysis; using System.Net; using McQuery.Net.Internal.Abstract; using McQuery.Net.Internal.Data; +using Microsoft.VisualStudio.Threading; namespace McQuery.Net.Internal.Providers; @@ -11,12 +12,17 @@ namespace McQuery.Net.Internal.Providers; /// . internal class SessionStorage(ISessionIdProvider sessionIdProvider) : ISessionStorage { + [SuppressMessage("Usage", "VSTHRD012:Provide JoinableTaskFactory where allowed")] + private readonly AsyncReaderWriterLock _locker = new(); + private IAuthOnlyClient? _authClient; - private readonly ConcurrentDictionary _sessionsByEndpoints = new(); + private readonly Dictionary _sessionsByEndpoints = new(); /// public async Task GetAsync(IPEndPoint serverEndpoint, CancellationToken cancellationToken = default) { + await using var releaser = await _locker.UpgradeableReadLockAsync(cancellationToken); + var sessionExists = _sessionsByEndpoints.TryGetValue(serverEndpoint, out var session); if (sessionExists && !session!.IsExpired) @@ -26,13 +32,13 @@ public async Task GetAsync(IPEndPoint serverEndpoint, CancellationToken if (sessionExists && session!.IsExpired) { - if (!_sessionsByEndpoints.TryRemove(serverEndpoint, out _)) + if (!_sessionsByEndpoints.Remove(serverEndpoint, out session)) { throw new Exception($"Cannot remove expired session {session} for some reason."); } } - return await AcquireSession(serverEndpoint, cancellationToken); + return await AcquireSessionAsync(serverEndpoint, cancellationToken); } internal void Init(IAuthOnlyClient client) @@ -42,8 +48,16 @@ internal void Init(IAuthOnlyClient client) _authClient = client; } - private async Task AcquireSession(IPEndPoint serverEndpoint, CancellationToken cancellationToken) + private async Task AcquireSessionAsync(IPEndPoint serverEndpoint, CancellationToken cancellationToken) { + await using var releaser = await _locker.WriteLockAsync(cancellationToken); + + var sessionExists = _sessionsByEndpoints.TryGetValue(serverEndpoint, out var currentSession); + if (sessionExists && !currentSession!.IsExpired) + { + return currentSession; + } + if (_authClient == null) { throw new InvalidOperationException("Storage must be initialized before calling this method."); @@ -53,12 +67,7 @@ private async Task AcquireSession(IPEndPoint serverEndpoint, Cancellati var challengeToken = await _authClient!.HandshakeAsync(serverEndpoint, sessionId, cancellationToken); Session session = new(sessionId, challengeToken); - if (!_sessionsByEndpoints.TryAdd(serverEndpoint, session)) - { - throw new Exception("Cannot add session for endpoint " + serverEndpoint + " for some reason."); - } - - return session; + return _sessionsByEndpoints[serverEndpoint] = session; } private bool _isDisposed; diff --git a/sources/McQuery.Net/McQuery.Net.csproj b/sources/McQuery.Net/McQuery.Net.csproj index 2f15a9e..e1b6119 100644 --- a/sources/McQuery.Net/McQuery.Net.csproj +++ b/sources/McQuery.Net/McQuery.Net.csproj @@ -6,6 +6,12 @@ + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/sources/McQuery.Net/McQueryClient.cs b/sources/McQuery.Net/McQueryClient.cs index 4d97c2f..e77affd 100644 --- a/sources/McQuery.Net/McQueryClient.cs +++ b/sources/McQuery.Net/McQueryClient.cs @@ -1,11 +1,15 @@ +using System.Diagnostics.CodeAnalysis; using System.Net; using System.Net.Sockets; using McQuery.Net.Data; using McQuery.Net.Internal.Abstract; using McQuery.Net.Internal.Data; using McQuery.Net.Internal.Factories; +using McQuery.Net.Internal.Helpers; using McQuery.Net.Internal.Parsers; using McQuery.Net.Internal.Providers; +using Microsoft.Extensions.Logging; +using Microsoft.VisualStudio.Threading; namespace McQuery.Net; @@ -15,49 +19,48 @@ namespace McQuery.Net; [UsedImplicitly] public class McQueryClient : IMcQueryClient, IAuthOnlyClient { + [SuppressMessage("Usage", "VSTHRD012:Provide JoinableTaskFactory where allowed")] + private readonly AsyncReaderWriterLock _locker = new(); + private readonly UdpClient _socket; private readonly IRequestFactory _requestFactory; private readonly ISessionStorage _sessionStorage; - private int _responseTimeoutSeconds = 5; // TODO + private readonly ILogger _logger; + + private const int ResponseTimeoutSeconds = 5; // TODO: into the config private static readonly IResponseParser handshakeResponseParser = new HandshakeResponseParser(); private static readonly IResponseParser basicStatusResponseParser = new BasicStatusResponseParser(); private static readonly IResponseParser fullStatusResponseParser = new FullStatusResponseParser(); - internal McQueryClient(UdpClient socket, IRequestFactory requestFactory, ISessionStorage sessionStorage) + internal McQueryClient( + UdpClient socket, + IRequestFactory requestFactory, + ISessionStorage sessionStorage, + ILogger logger + ) { _requestFactory = requestFactory; _sessionStorage = sessionStorage; + _logger = logger; _socket = socket; } /// - public async Task GetBasicStatusAsync(IPEndPoint serverEndpoint, CancellationToken cancellationToken) - { - return await SendRequestAsync( + public async Task GetBasicStatusAsync(IPEndPoint serverEndpoint, CancellationToken cancellationToken) => + await SendRequestAsync( serverEndpoint, session => _requestFactory.GetBasicStatusRequest(session), basicStatusResponseParser, cancellationToken); - } /// - public async Task GetFullStatusAsync(IPEndPoint serverEndpoint, CancellationToken cancellationToken) - { - return await SendRequestAsync( + public async Task GetFullStatusAsync(IPEndPoint serverEndpoint, CancellationToken cancellationToken) => + await SendRequestAsync( serverEndpoint, session => _requestFactory.GetFullStatusRequest(session), fullStatusResponseParser, cancellationToken); - } - - /// - public BasicStatus GetBasicStatus(IPEndPoint serverEndpoint, CancellationToken cancellationToken) => - GetBasicStatusAsync(serverEndpoint, cancellationToken).GetAwaiter().GetResult(); - - /// - public FullStatus GetFullStatus(IPEndPoint serverEndpoint, CancellationToken cancellationToken) => - GetFullStatusAsync(serverEndpoint, cancellationToken).GetAwaiter().GetResult(); /// async Task IAuthOnlyClient.HandshakeAsync( @@ -67,7 +70,6 @@ CancellationToken cancellationToken ) { var packet = _requestFactory.GetHandshakeRequest(sessionId); - return await SendRequestAsync( serverEndpoint, packet, @@ -98,26 +100,38 @@ private async Task SendRequestAsync( CancellationToken cancellationToken = default ) { - Console.WriteLine( - $"Sending {packet.Length} bytes to {serverEndpoint} with content {BitConverter.ToString(packet.ToArray())}"); - - await _socket.SendAsync(packet, serverEndpoint, cancellationToken).ConfigureAwait(continueOnCapturedContext: false); + _logger.LogDebug( + "Sending {PacketLength} bytes to {Endpoint} with content {Content}", + packet.Length, + serverEndpoint, + BitConverter.ToString(packet.ToArray())); - using CancellationTokenSource timeoutSource = new(TimeSpan.FromSeconds(_responseTimeoutSeconds)); - using var linkedSource = - CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutSource.Token); + var response = await ExecuteRequestConcurrentlyAsync(); - var tokenWithTimeout = linkedSource.Token; - // TODO: common response pool - var response = await _socket.ReceiveAsync(tokenWithTimeout).ConfigureAwait(continueOnCapturedContext: false); + _logger.LogDebug( + "Received response from server {Endpoint} [{Content}]", + serverEndpoint, + BitConverter.ToString(response.Buffer)); - Console.WriteLine($"Received response from server: {BitConverter.ToString(response.Buffer)}"); var responseData = responseParser.Parse(response.Buffer); - Console.WriteLine(responseData); + _logger.LogDebug( + "Parsed response from server {Endpoint} \n{Response}", + serverEndpoint, + responseData); return responseData; + + async Task ExecuteRequestConcurrentlyAsync() + { + using var timeoutSource = cancellationToken.ToSourceWithTimeout(TimeSpan.FromSeconds(ResponseTimeoutSeconds)); + await using var _ = await _locker.WriteLockAsync(timeoutSource.Token); + + await _socket.SendAsync(packet, serverEndpoint, cancellationToken).ConfigureAwait(continueOnCapturedContext: false); + return await _socket.ReceiveAsync(timeoutSource.Token).ConfigureAwait(continueOnCapturedContext: false); + } } + private bool _isDisposed; public void Dispose() diff --git a/sources/McQuery.Net/McQueryClientFactory.cs b/sources/McQuery.Net/McQueryClientFactory.cs index 7e6aacb..b417744 100644 --- a/sources/McQuery.Net/McQueryClientFactory.cs +++ b/sources/McQuery.Net/McQueryClientFactory.cs @@ -1,22 +1,37 @@ using System.Net.Sockets; using McQuery.Net.Internal.Factories; using McQuery.Net.Internal.Providers; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; namespace McQuery.Net; [UsedImplicitly] public class McQueryClientFactory : IMcQueryClientFactory { - private readonly Lazy _sessionIdProvider = new(() => new SessionIdProvider(), isThreadSafe: true); + private readonly ILoggerFactory? _loggerFactory; + private readonly Lazy _sessionIdProvider; + private readonly Lazy _client; - public IMcQueryClient Get() + public McQueryClientFactory(ILoggerFactory? loggerFactory = null) + { + _loggerFactory = loggerFactory; + _sessionIdProvider = new Lazy(() => new SessionIdProvider(), isThreadSafe: true); + _client = new Lazy(AcquireClient, isThreadSafe: true); + } + + public IMcQueryClient Get() => _client.Value; + + + private IMcQueryClient AcquireClient() { SessionStorage sessionStorage = new(_sessionIdProvider.Value); McQueryClient client = new( new UdpClient(), new RequestFactory(), - sessionStorage); + sessionStorage, + _loggerFactory?.CreateLogger() ?? new NullLogger()); sessionStorage.Init(client); return client; From 39be8a46f93ded6b88da1e8a26c9f47cac5d5e75 Mon Sep 17 00:00:00 2001 From: Maxim Liven Date: Tue, 19 Nov 2024 14:30:22 +0300 Subject: [PATCH 12/31] fix: circular disposing --- sources/McQuery.Net/Internal/Providers/SessionStorage.cs | 3 +-- sources/McQuery.Net/McQueryClient.cs | 4 +--- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/sources/McQuery.Net/Internal/Providers/SessionStorage.cs b/sources/McQuery.Net/Internal/Providers/SessionStorage.cs index 4b5bff6..1697ac9 100644 --- a/sources/McQuery.Net/Internal/Providers/SessionStorage.cs +++ b/sources/McQuery.Net/Internal/Providers/SessionStorage.cs @@ -71,13 +71,12 @@ private async Task AcquireSessionAsync(IPEndPoint serverEndpoint, Cance } private bool _isDisposed; - public void Dispose() { if (_isDisposed) return; + _isDisposed = true; _authClient?.Dispose(); GC.SuppressFinalize(this); - _isDisposed = true; } } diff --git a/sources/McQuery.Net/McQueryClient.cs b/sources/McQuery.Net/McQueryClient.cs index e77affd..8feb934 100644 --- a/sources/McQuery.Net/McQueryClient.cs +++ b/sources/McQuery.Net/McQueryClient.cs @@ -131,16 +131,14 @@ async Task ExecuteRequestConcurrentlyAsync() } } - private bool _isDisposed; - public void Dispose() { if (_isDisposed) return; + _isDisposed = true; _socket.Dispose(); _sessionStorage.Dispose(); GC.SuppressFinalize(this); - _isDisposed = true; } } From dcb96529801d1632fcbf4ee969162e7a89ca6c01 Mon Sep 17 00:00:00 2001 From: Maxim Liven Date: Tue, 19 Nov 2024 14:30:48 +0300 Subject: [PATCH 13/31] refactor: small fixes --- Directory.Packages.props | 10 +++++----- .../Helpers/CancellationTokenTimeoutEnricher.cs | 5 +++-- sources/McQuery.Net/McQuery.Net.csproj | 4 ++-- sources/McQuery.Net/McQueryClientFactory.cs | 3 +-- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index c226878..d36e836 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -3,10 +3,10 @@ true - - - - - + + + + + \ No newline at end of file diff --git a/sources/McQuery.Net/Internal/Helpers/CancellationTokenTimeoutEnricher.cs b/sources/McQuery.Net/Internal/Helpers/CancellationTokenTimeoutEnricher.cs index ae71c8e..0d87c95 100644 --- a/sources/McQuery.Net/Internal/Helpers/CancellationTokenTimeoutEnricher.cs +++ b/sources/McQuery.Net/Internal/Helpers/CancellationTokenTimeoutEnricher.cs @@ -27,11 +27,12 @@ public static CancellationTokenSourceWithTimeout Create(CancellationToken cancel return new CancellationTokenSourceWithTimeout( timeoutSource, CancellationTokenSource.CreateLinkedTokenSource( - cancellationToken, - timeoutSource.Token)); + cancellationToken, + timeoutSource.Token)); } private bool _isDisposed = false; + public void Dispose() { if (_isDisposed) return; diff --git a/sources/McQuery.Net/McQuery.Net.csproj b/sources/McQuery.Net/McQuery.Net.csproj index e1b6119..0528dfa 100644 --- a/sources/McQuery.Net/McQuery.Net.csproj +++ b/sources/McQuery.Net/McQuery.Net.csproj @@ -6,8 +6,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/sources/McQuery.Net/McQueryClientFactory.cs b/sources/McQuery.Net/McQueryClientFactory.cs index b417744..2498d3c 100644 --- a/sources/McQuery.Net/McQueryClientFactory.cs +++ b/sources/McQuery.Net/McQueryClientFactory.cs @@ -17,12 +17,11 @@ public McQueryClientFactory(ILoggerFactory? loggerFactory = null) { _loggerFactory = loggerFactory; _sessionIdProvider = new Lazy(() => new SessionIdProvider(), isThreadSafe: true); - _client = new Lazy(AcquireClient, isThreadSafe: true); + _client = new Lazy(AcquireClient, isThreadSafe: true); } public IMcQueryClient Get() => _client.Value; - private IMcQueryClient AcquireClient() { SessionStorage sessionStorage = new(_sessionIdProvider.Value); From 1ed0e5027f305eec9826cc7945225f5f696c5b07 Mon Sep 17 00:00:00 2001 From: Maxim Liven Date: Tue, 19 Nov 2024 14:31:55 +0300 Subject: [PATCH 14/31] feat: add simple sample project --- McQuery.Net.slnx | 1 + .../McQuery.Net.Samples.csproj | 18 ++++++ sources/McQuery.Net.Samples/Program.cs | 63 +++++++++++++++++++ 3 files changed, 82 insertions(+) create mode 100644 sources/McQuery.Net.Samples/McQuery.Net.Samples.csproj create mode 100644 sources/McQuery.Net.Samples/Program.cs diff --git a/McQuery.Net.slnx b/McQuery.Net.slnx index cb85641..42c05bf 100644 --- a/McQuery.Net.slnx +++ b/McQuery.Net.slnx @@ -1,3 +1,4 @@  + diff --git a/sources/McQuery.Net.Samples/McQuery.Net.Samples.csproj b/sources/McQuery.Net.Samples/McQuery.Net.Samples.csproj new file mode 100644 index 0000000..6e3651a --- /dev/null +++ b/sources/McQuery.Net.Samples/McQuery.Net.Samples.csproj @@ -0,0 +1,18 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + + + + + diff --git a/sources/McQuery.Net.Samples/Program.cs b/sources/McQuery.Net.Samples/Program.cs new file mode 100644 index 0000000..459497e --- /dev/null +++ b/sources/McQuery.Net.Samples/Program.cs @@ -0,0 +1,63 @@ +using System.Diagnostics; +using System.Net; +using McQuery.Net; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +var serviceProvider = new ServiceCollection() + .AddLogging( + builder => + { + builder.SetMinimumLevel(LogLevel.Information); + builder.AddConsole(); + }) + .AddSingleton() + .BuildServiceProvider(); + +var factory = serviceProvider.GetRequiredService(); +using var client = factory.Get(); + +int[] ports = [25565, 25566, 25567]; +Func[] commandFactories = +[ + ep => new BasicStatusCommand(ep), + ep => new FullStatusCommand(ep), +]; +CommandBase[] commands = +[ + .. + from _ in Enumerable.Range(start: 0, count: 5000) + from fc in commandFactories + from port in ports + select fc(new IPEndPoint(IPAddress.Loopback, port)), +]; +Random.Shared.Shuffle(commands); + +var logger = serviceProvider.GetRequiredService>(); +logger.IsEnabled(LogLevel.Trace); +logger.LogInformation("Starting McQuery.Net.Sample with {Count} requests", commands.Length); +var stopwatch = Stopwatch.StartNew(); +await Task.WhenAll(commands.Select(x => x.ExecuteAsync(client)).ToArray()); +stopwatch.Stop(); +logger.LogInformation("Finished. It took {Elapsed}", stopwatch.Elapsed); + +public abstract class CommandBase(IPEndPoint endPoint) +{ + public abstract Task ExecuteAsync(IMcQueryClient client, CancellationToken cancellationToken = default); +} + +public class BasicStatusCommand(IPEndPoint endPoint) : CommandBase(endPoint) +{ + private readonly IPEndPoint _endPoint = endPoint; + + public override Task ExecuteAsync(IMcQueryClient client, CancellationToken cancellationToken = default) => + client.GetBasicStatusAsync(_endPoint, cancellationToken); +} + +public class FullStatusCommand(IPEndPoint endPoint) : CommandBase(endPoint) +{ + private readonly IPEndPoint _endPoint = endPoint; + + public override Task ExecuteAsync(IMcQueryClient client, CancellationToken cancellationToken = default) => + client.GetFullStatusAsync(_endPoint, cancellationToken); +} From e2b47b84bbd3a376e95ec371ef63310085231d36 Mon Sep 17 00:00:00 2001 From: Maxim Liven Date: Tue, 19 Nov 2024 15:53:47 +0300 Subject: [PATCH 15/31] refactor: small changes --- Directory.Packages.props | 14 ++++++---- .../McQuery.Net.Samples.csproj | 13 +++++++--- sources/McQuery.Net.Samples/Program.cs | 26 ++++++++++++++----- sources/McQuery.Net.Samples/logging.json | 14 ++++++++++ .../Internal/Providers/SessionStorage.cs | 1 + sources/McQuery.Net/McQueryClient.cs | 1 + 6 files changed, 55 insertions(+), 14 deletions(-) create mode 100644 sources/McQuery.Net.Samples/logging.json diff --git a/Directory.Packages.props b/Directory.Packages.props index d36e836..62c6997 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -3,10 +3,14 @@ true - - - - - + + + + + + + + + \ No newline at end of file diff --git a/sources/McQuery.Net.Samples/McQuery.Net.Samples.csproj b/sources/McQuery.Net.Samples/McQuery.Net.Samples.csproj index 6e3651a..c1c4c1e 100644 --- a/sources/McQuery.Net.Samples/McQuery.Net.Samples.csproj +++ b/sources/McQuery.Net.Samples/McQuery.Net.Samples.csproj @@ -2,9 +2,6 @@ Exe - net8.0 - enable - enable @@ -12,7 +9,17 @@ + + + + + + + + + Always + diff --git a/sources/McQuery.Net.Samples/Program.cs b/sources/McQuery.Net.Samples/Program.cs index 459497e..4092525 100644 --- a/sources/McQuery.Net.Samples/Program.cs +++ b/sources/McQuery.Net.Samples/Program.cs @@ -1,14 +1,20 @@ using System.Diagnostics; using System.Net; using McQuery.Net; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +var loggingConfiguration = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("logging.json", optional: false, reloadOnChange: true) + .Build(); var serviceProvider = new ServiceCollection() .AddLogging( builder => { - builder.SetMinimumLevel(LogLevel.Information); + builder.AddConfiguration(loggingConfiguration.GetSection("Logging")); + builder.SetMinimumLevel(LogLevel.Debug); builder.AddConsole(); }) .AddSingleton() @@ -35,11 +41,19 @@ select fc(new IPEndPoint(IPAddress.Loopback, port)), var logger = serviceProvider.GetRequiredService>(); logger.IsEnabled(LogLevel.Trace); -logger.LogInformation("Starting McQuery.Net.Sample with {Count} requests", commands.Length); -var stopwatch = Stopwatch.StartNew(); -await Task.WhenAll(commands.Select(x => x.ExecuteAsync(client)).ToArray()); -stopwatch.Stop(); -logger.LogInformation("Finished. It took {Elapsed}", stopwatch.Elapsed); +try +{ + logger.LogInformation("Starting McQuery.Net.Sample with {Count} requests", commands.Length); + var stopwatch = Stopwatch.StartNew(); + await Task.WhenAll(commands.Select(x => x.ExecuteAsync(client)).ToArray()); + stopwatch.Stop(); + logger.LogInformation("Finished. It took {Elapsed}", stopwatch.Elapsed); +} +catch (Exception ex) +{ + logger.LogError(ex, "Cannot finish calculating McQuery.Net.Sample"); + throw; +} public abstract class CommandBase(IPEndPoint endPoint) { diff --git a/sources/McQuery.Net.Samples/logging.json b/sources/McQuery.Net.Samples/logging.json new file mode 100644 index 0000000..d983ea9 --- /dev/null +++ b/sources/McQuery.Net.Samples/logging.json @@ -0,0 +1,14 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Debug", + "System": "Information", + "Microsoft": "Information" + }, + "Console": { + "IncludeScopes": "true", + "TimestampFormat": "[HH:mm:ss] ", + "LogToStandardErrorThreshold": "Warning" + } + } +} diff --git a/sources/McQuery.Net/Internal/Providers/SessionStorage.cs b/sources/McQuery.Net/Internal/Providers/SessionStorage.cs index 1697ac9..e7dbe4e 100644 --- a/sources/McQuery.Net/Internal/Providers/SessionStorage.cs +++ b/sources/McQuery.Net/Internal/Providers/SessionStorage.cs @@ -71,6 +71,7 @@ private async Task AcquireSessionAsync(IPEndPoint serverEndpoint, Cance } private bool _isDisposed; + public void Dispose() { if (_isDisposed) return; diff --git a/sources/McQuery.Net/McQueryClient.cs b/sources/McQuery.Net/McQueryClient.cs index 8feb934..af028b9 100644 --- a/sources/McQuery.Net/McQueryClient.cs +++ b/sources/McQuery.Net/McQueryClient.cs @@ -132,6 +132,7 @@ async Task ExecuteRequestConcurrentlyAsync() } private bool _isDisposed; + public void Dispose() { if (_isDisposed) return; From 91594ef7a3fd8f129da88e4d6be36ae2cf1a8e2c Mon Sep 17 00:00:00 2001 From: Maxim Liven Date: Tue, 19 Nov 2024 16:25:27 +0300 Subject: [PATCH 16/31] feat: add github actions --- .github/workflows/nuget_publish.yaml | 115 +++++++++++++++++++++++++++ Directory.Build.props | 6 ++ 2 files changed, 121 insertions(+) create mode 100644 .github/workflows/nuget_publish.yaml diff --git a/.github/workflows/nuget_publish.yaml b/.github/workflows/nuget_publish.yaml new file mode 100644 index 0000000..426c7ba --- /dev/null +++ b/.github/workflows/nuget_publish.yaml @@ -0,0 +1,115 @@ +name: publish nuget +on: + workflow_dispatch: + push: + branches: + - 'main' + pull_request: + branches: + - '*' + release: + types: + - published + +env: + DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 1 + DOTNET_NOLOGO: true + NuGetDirectory: ${{github.workspace}}/nuget + DOTNET_TARGET_VERSION: 8.0.x + VERSION_SUFFIX: ${{ github.run_number }} + +defaults: + run: + shell: pwsh + +jobs: + create_nuget: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 # Get all history to allow automatic versioning using MinVer + + # Install the .NET SDK indicated in the global.json file + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: ${{ env.DOTNET_TARGET_VERSION }} + + + # Create the NuGet package in the folder from the environment variable NuGetDirectory + - run: dotnet pack --version-suffix ${{ env.VERSION_SUFFIX }} --configuration Release --output ${{ env.NuGetDirectory }} + + # Publish the NuGet package as an artifact, so they can be used in the following jobs + - uses: actions/upload-artifact@v3 + with: + name: nuget + if-no-files-found: error + retention-days: 7 + path: ${{ env.NuGetDirectory }}/*.nupkg + + validate_nuget: + runs-on: ubuntu-latest + needs: [ create_nuget ] + steps: + # Install the .NET SDK indicated in the global.json file + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: ${{ env.DOTNET_TARGET_VERSION }} + + # Download the NuGet package created in the previous job + - uses: actions/download-artifact@v3 + with: + name: nuget + path: ${{ env.NuGetDirectory }} + + - name: Install nuget validator + run: dotnet tool update Meziantou.Framework.NuGetPackageValidation.Tool --global + + # Validate metadata and content of the NuGet package + # https://www.nuget.org/packages/Meziantou.Framework.NuGetPackageValidation.Tool#readme-body-tab + # If some rules are not applicable, you can disable them + # using the --excluded-rules or --excluded-rule-ids option + - name: Validate package + run: meziantou.validate-nuget-package (Get-ChildItem "${{ env.NuGetDirectory }}/*.nupkg") + + run_test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: ${{ env.DOTNET_TARGET_VERSION }} + - name: Run tests + run: dotnet test --configuration Release + + deploy: + # Publish only when creating a GitHub Release + # https://docs.github.com/en/repositories/releasing-projects-on-github/managing-releases-in-a-repository + # You can update this logic if you want to manage releases differently + if: github.event_name == 'release' + runs-on: ubuntu-latest + needs: [ validate_nuget, run_test ] + steps: + # Download the NuGet package created in the previous job + - uses: actions/download-artifact@v3 + with: + name: nuget + path: ${{ env.NuGetDirectory }} + + # Install the .NET SDK indicated in the global.json file + - name: Setup .NET Core + uses: actions/setup-dotnet@v4 + with: + dotnet-version: ${{ env.DOTNET_TARGET_VERSION }} + + # Publish all NuGet packages to NuGet.org + # Use --skip-duplicate to prevent errors if a package with the same version already exists. + # If you retry a failed workflow, already published packages will be skipped without error. + - name: Publish NuGet package + run: | + foreach($file in (Get-ChildItem "${{ env.NuGetDirectory }}" -Recurse -Include *.nupkg)) { + dotnet nuget push $file --api-key "${{ secrets.NUGET_API_TOKEN }}" --source https://api.nuget.org/v3/index.json --skip-duplicate + } diff --git a/Directory.Build.props b/Directory.Build.props index dbeb937..054031c 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -13,6 +13,12 @@ + 2.0.0 + + + + + $(VersionPrefix)$(VersionSuffix) MaxLevs Copyright (c) 2021-$([System.DateTime]::UtcNow.ToString('yyyy')) $(Authors) From 7650b2c87bbebc767f23f6979172c9c84d28ebfd Mon Sep 17 00:00:00 2001 From: Maxim Liven Date: Tue, 19 Nov 2024 16:38:28 +0300 Subject: [PATCH 17/31] fix: package build --- .github/workflows/nuget_publish.yaml | 2 +- McQuery.Net.sln | 28 ++++++++++++++++++++++++++ McQuery.Net.slnx | 4 ---- sources/McQuery.Net/McQuery.Net.csproj | 5 ----- 4 files changed, 29 insertions(+), 10 deletions(-) create mode 100644 McQuery.Net.sln delete mode 100644 McQuery.Net.slnx diff --git a/.github/workflows/nuget_publish.yaml b/.github/workflows/nuget_publish.yaml index 426c7ba..e58cc1d 100644 --- a/.github/workflows/nuget_publish.yaml +++ b/.github/workflows/nuget_publish.yaml @@ -38,7 +38,7 @@ jobs: # Create the NuGet package in the folder from the environment variable NuGetDirectory - - run: dotnet pack --version-suffix ${{ env.VERSION_SUFFIX }} --configuration Release --output ${{ env.NuGetDirectory }} + - run: dotnet pack --version-suffix ".${{ env.VERSION_SUFFIX }}" --configuration Release --output ${{ env.NuGetDirectory }} # Publish the NuGet package as an artifact, so they can be used in the following jobs - uses: actions/upload-artifact@v3 diff --git a/McQuery.Net.sln b/McQuery.Net.sln new file mode 100644 index 0000000..82b85ec --- /dev/null +++ b/McQuery.Net.sln @@ -0,0 +1,28 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "McQuery.Net", "sources\McQuery.Net\McQuery.Net.csproj", "{2DDC52DA-E5E5-4685-9DFB-08365C6C1771}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "McQuery.Net.Samples", "sources\McQuery.Net.Samples\McQuery.Net.Samples.csproj", "{4264FB51-B938-46ED-907A-AAD0AC9363E7}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {2DDC52DA-E5E5-4685-9DFB-08365C6C1771}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2DDC52DA-E5E5-4685-9DFB-08365C6C1771}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2DDC52DA-E5E5-4685-9DFB-08365C6C1771}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2DDC52DA-E5E5-4685-9DFB-08365C6C1771}.Release|Any CPU.Build.0 = Release|Any CPU + {4264FB51-B938-46ED-907A-AAD0AC9363E7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4264FB51-B938-46ED-907A-AAD0AC9363E7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4264FB51-B938-46ED-907A-AAD0AC9363E7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4264FB51-B938-46ED-907A-AAD0AC9363E7}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/McQuery.Net.slnx b/McQuery.Net.slnx deleted file mode 100644 index 42c05bf..0000000 --- a/McQuery.Net.slnx +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/sources/McQuery.Net/McQuery.Net.csproj b/sources/McQuery.Net/McQuery.Net.csproj index 0528dfa..e703276 100644 --- a/sources/McQuery.Net/McQuery.Net.csproj +++ b/sources/McQuery.Net/McQuery.Net.csproj @@ -5,7 +5,6 @@ - @@ -14,8 +13,4 @@ - - - - From 893463da9536ed5653cd3ca25ad330e9646de91b Mon Sep 17 00:00:00 2001 From: Liven Maxim Date: Tue, 19 Nov 2024 19:42:05 +0300 Subject: [PATCH 18/31] chore: remove obsolete files --- sources/McQuery.Net/Utils/ByteCounter.cs | 35 ------------------------ 1 file changed, 35 deletions(-) delete mode 100644 sources/McQuery.Net/Utils/ByteCounter.cs diff --git a/sources/McQuery.Net/Utils/ByteCounter.cs b/sources/McQuery.Net/Utils/ByteCounter.cs deleted file mode 100644 index ca260e0..0000000 --- a/sources/McQuery.Net/Utils/ByteCounter.cs +++ /dev/null @@ -1,35 +0,0 @@ -namespace McQuery.Net.Utils; - -internal class ByteCounter -{ - private readonly byte[] countUnits; - - public ByteCounter() - { - countUnits = new byte[4]; - Reset(); - } - - public bool GetNext(byte[] receiver) - { - for (int i = 0; i < countUnits.Length; ++i) - { - if (countUnits[i] < 0x0F) - { - countUnits[i]++; - countUnits.CopyTo(receiver, 0); - - return true; - } - - countUnits[i] = 0x00; - } - - return false; - } - - public void Reset() - { - for (int i = 0; i < countUnits.Length; ++i) countUnits[i] = 0; - } -} From ee837e87bdece240c81f758faa59938be23bf25d Mon Sep 17 00:00:00 2001 From: Maxim Liven Date: Tue, 19 Nov 2024 19:52:01 +0300 Subject: [PATCH 19/31] chore: add dict --- McQuery.Net.sln.DotSettings | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 McQuery.Net.sln.DotSettings diff --git a/McQuery.Net.sln.DotSettings b/McQuery.Net.sln.DotSettings new file mode 100644 index 0000000..99b124e --- /dev/null +++ b/McQuery.Net.sln.DotSettings @@ -0,0 +1,2 @@ + + True \ No newline at end of file From 8c88eef2000ce15c998f3a7cef500255bc9c61f6 Mon Sep 17 00:00:00 2001 From: Maxim Liven Date: Tue, 19 Nov 2024 19:52:27 +0300 Subject: [PATCH 20/31] build(fix): rename VERSION_SUFFIX env to BUILD_NUMBER --- .github/workflows/nuget_publish.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/nuget_publish.yaml b/.github/workflows/nuget_publish.yaml index e58cc1d..9e387f3 100644 --- a/.github/workflows/nuget_publish.yaml +++ b/.github/workflows/nuget_publish.yaml @@ -16,7 +16,7 @@ env: DOTNET_NOLOGO: true NuGetDirectory: ${{github.workspace}}/nuget DOTNET_TARGET_VERSION: 8.0.x - VERSION_SUFFIX: ${{ github.run_number }} + BUILD_NUMBER: ${{ github.run_number }} defaults: run: @@ -38,7 +38,7 @@ jobs: # Create the NuGet package in the folder from the environment variable NuGetDirectory - - run: dotnet pack --version-suffix ".${{ env.VERSION_SUFFIX }}" --configuration Release --output ${{ env.NuGetDirectory }} + - run: dotnet pack --version-suffix ".${{ env.BUILD_NUMBER }}" --configuration Release --output ${{ env.NuGetDirectory }} # Publish the NuGet package as an artifact, so they can be used in the following jobs - uses: actions/upload-artifact@v3 From 65c37ab2bd227be84cd54b662f36dbb8372ae576 Mon Sep 17 00:00:00 2001 From: Maxim Liven Date: Tue, 19 Nov 2024 20:40:21 +0300 Subject: [PATCH 21/31] fix: naming --- .../{AlreadyExpiredException.cs => ExpiredException.cs} | 6 +++--- sources/McQuery.Net/Internal/Data/ChallengeToken.cs | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) rename sources/McQuery.Net/Exceptions/{AlreadyExpiredException.cs => ExpiredException.cs} (55%) diff --git a/sources/McQuery.Net/Exceptions/AlreadyExpiredException.cs b/sources/McQuery.Net/Exceptions/ExpiredException.cs similarity index 55% rename from sources/McQuery.Net/Exceptions/AlreadyExpiredException.cs rename to sources/McQuery.Net/Exceptions/ExpiredException.cs index 119fa56..95ade33 100644 --- a/sources/McQuery.Net/Exceptions/AlreadyExpiredException.cs +++ b/sources/McQuery.Net/Exceptions/ExpiredException.cs @@ -3,15 +3,15 @@ namespace McQuery.Net.Exceptions; [PublicAPI] -public class AlreadyExpiredException : ArgumentException +public class ExpiredException : ArgumentException { - internal AlreadyExpiredException(IExpirable expirable) + internal ExpiredException(IExpirable expirable) : base($"{expirable.GetType().Name} is already expired") { } internal static void ThrowIfExpired(IExpirable expirable) { - if (expirable.IsExpired) throw new AlreadyExpiredException(expirable); + if (expirable.IsExpired) throw new ExpiredException(expirable); } } diff --git a/sources/McQuery.Net/Internal/Data/ChallengeToken.cs b/sources/McQuery.Net/Internal/Data/ChallengeToken.cs index 8008cdb..caa45cd 100644 --- a/sources/McQuery.Net/Internal/Data/ChallengeToken.cs +++ b/sources/McQuery.Net/Internal/Data/ChallengeToken.cs @@ -33,7 +33,7 @@ public ChallengeToken(byte[] data) public static implicit operator byte[](ChallengeToken token) { - AlreadyExpiredException.ThrowIfExpired(token); + ExpiredException.ThrowIfExpired(token); return [..token.Data]; } From 5a6e57a03c08128d2ff95c28953610188f378f03 Mon Sep 17 00:00:00 2001 From: Maxim Liven Date: Wed, 20 Nov 2024 08:47:07 +0300 Subject: [PATCH 22/31] chore: small fixes in samples --- sources/McQuery.Net.Samples/Program.cs | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/sources/McQuery.Net.Samples/Program.cs b/sources/McQuery.Net.Samples/Program.cs index 4092525..9348a92 100644 --- a/sources/McQuery.Net.Samples/Program.cs +++ b/sources/McQuery.Net.Samples/Program.cs @@ -55,23 +55,21 @@ select fc(new IPEndPoint(IPAddress.Loopback, port)), throw; } -public abstract class CommandBase(IPEndPoint endPoint) +abstract file class CommandBase(IPEndPoint endPoint) { + protected readonly IPEndPoint EndPoint = endPoint; + public abstract Task ExecuteAsync(IMcQueryClient client, CancellationToken cancellationToken = default); } -public class BasicStatusCommand(IPEndPoint endPoint) : CommandBase(endPoint) +file class BasicStatusCommand(IPEndPoint endPoint) : CommandBase(endPoint) { - private readonly IPEndPoint _endPoint = endPoint; - public override Task ExecuteAsync(IMcQueryClient client, CancellationToken cancellationToken = default) => - client.GetBasicStatusAsync(_endPoint, cancellationToken); + client.GetBasicStatusAsync(EndPoint, cancellationToken); } -public class FullStatusCommand(IPEndPoint endPoint) : CommandBase(endPoint) +file class FullStatusCommand(IPEndPoint endPoint) : CommandBase(endPoint) { - private readonly IPEndPoint _endPoint = endPoint; - public override Task ExecuteAsync(IMcQueryClient client, CancellationToken cancellationToken = default) => - client.GetFullStatusAsync(_endPoint, cancellationToken); + client.GetFullStatusAsync(EndPoint, cancellationToken); } From e0592db2ade83537635bcd251f6007fc660d58a1 Mon Sep 17 00:00:00 2001 From: Maxim Liven Date: Wed, 20 Nov 2024 09:17:32 +0300 Subject: [PATCH 23/31] fix: ci --- .github/workflows/nuget_publish.yaml | 18 +++++++++++++++--- Directory.Build.props | 11 ++++++----- sources/McQuery.Net/McQuery.Net.csproj | 6 ++++++ 3 files changed, 27 insertions(+), 8 deletions(-) diff --git a/.github/workflows/nuget_publish.yaml b/.github/workflows/nuget_publish.yaml index 9e387f3..8e07bed 100644 --- a/.github/workflows/nuget_publish.yaml +++ b/.github/workflows/nuget_publish.yaml @@ -38,7 +38,13 @@ jobs: # Create the NuGet package in the folder from the environment variable NuGetDirectory - - run: dotnet pack --version-suffix ".${{ env.BUILD_NUMBER }}" --configuration Release --output ${{ env.NuGetDirectory }} + - name: Build packages + run: | + dotnet pack \ + --property:ContinuousIntegrationBuild=true \ + --version-suffix ".${{ env.BUILD_NUMBER }}" \ + --configuration Release \ + --output ${{ env.NuGetDirectory }} # Publish the NuGet package as an artifact, so they can be used in the following jobs - uses: actions/upload-artifact@v3 @@ -46,7 +52,9 @@ jobs: name: nuget if-no-files-found: error retention-days: 7 - path: ${{ env.NuGetDirectory }}/*.nupkg + path: | + ${{ env.NuGetDirectory }}/*.nupkg + ${{ env.NuGetDirectory }}/*.snupkg validate_nuget: runs-on: ubuntu-latest @@ -72,7 +80,11 @@ jobs: # If some rules are not applicable, you can disable them # using the --excluded-rules or --excluded-rule-ids option - name: Validate package - run: meziantou.validate-nuget-package (Get-ChildItem "${{ env.NuGetDirectory }}/*.nupkg") + run: | + meziantou.validate-nuget-package \ + (Get-ChildItem "${{ env.NuGetDirectory }}/*.nupkg") \ + --excluded-rules LicenseMustBeSet + run_test: runs-on: ubuntu-latest diff --git a/Directory.Build.props b/Directory.Build.props index 02f5f15..c842840 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -18,15 +18,16 @@ - $(VersionPrefix)$(VersionSuffix) MaxLevs - Copyright (c) 2021-$([System.DateTime]::UtcNow.ToString('yyyy')) $(Authors) - - Library for .Net which implements Minecraft Query protocol. You can use it for getting statuses of a Minecraft server. - minecraft, query, client icon.png README.md + Copyright (c) 2021-$([System.DateTime]::UtcNow.ToString('yyyy')) $(Authors) + $(VersionPrefix)$(VersionSuffix) + https://github.com/MaxLevs/McQuery.Net + $(BasicPackageUrl) + $(BasicPackageUrl) + git diff --git a/sources/McQuery.Net/McQuery.Net.csproj b/sources/McQuery.Net/McQuery.Net.csproj index e703276..f27d73f 100644 --- a/sources/McQuery.Net/McQuery.Net.csproj +++ b/sources/McQuery.Net/McQuery.Net.csproj @@ -1,5 +1,11 @@  + + + Library for .Net which implements Minecraft Query protocol. You can use it for getting statuses of a Minecraft server. + + + true From b398c37ed4ebaf3f72a7eaa1384afc2566d355dd Mon Sep 17 00:00:00 2001 From: Maxim Liven Date: Wed, 20 Nov 2024 09:20:40 +0300 Subject: [PATCH 24/31] fix: line endings --- .github/workflows/nuget_publish.yaml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/nuget_publish.yaml b/.github/workflows/nuget_publish.yaml index 8e07bed..74663fb 100644 --- a/.github/workflows/nuget_publish.yaml +++ b/.github/workflows/nuget_publish.yaml @@ -40,10 +40,10 @@ jobs: # Create the NuGet package in the folder from the environment variable NuGetDirectory - name: Build packages run: | - dotnet pack \ - --property:ContinuousIntegrationBuild=true \ - --version-suffix ".${{ env.BUILD_NUMBER }}" \ - --configuration Release \ + dotnet pack ' + --property:ContinuousIntegrationBuild=true ' + --version-suffix ".${{ env.BUILD_NUMBER }}" ' + --configuration Release ' --output ${{ env.NuGetDirectory }} # Publish the NuGet package as an artifact, so they can be used in the following jobs @@ -81,8 +81,8 @@ jobs: # using the --excluded-rules or --excluded-rule-ids option - name: Validate package run: | - meziantou.validate-nuget-package \ - (Get-ChildItem "${{ env.NuGetDirectory }}/*.nupkg") \ + meziantou.validate-nuget-package ' + (Get-ChildItem "${{ env.NuGetDirectory }}/*.nupkg") ' --excluded-rules LicenseMustBeSet From e81c6ef570f294de915e384e85187d1165be75aa Mon Sep 17 00:00:00 2001 From: Maxim Liven Date: Wed, 20 Nov 2024 09:22:23 +0300 Subject: [PATCH 25/31] fix: broken pipline --- .github/workflows/nuget_publish.yaml | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/.github/workflows/nuget_publish.yaml b/.github/workflows/nuget_publish.yaml index 74663fb..1f27b89 100644 --- a/.github/workflows/nuget_publish.yaml +++ b/.github/workflows/nuget_publish.yaml @@ -39,12 +39,7 @@ jobs: # Create the NuGet package in the folder from the environment variable NuGetDirectory - name: Build packages - run: | - dotnet pack ' - --property:ContinuousIntegrationBuild=true ' - --version-suffix ".${{ env.BUILD_NUMBER }}" ' - --configuration Release ' - --output ${{ env.NuGetDirectory }} + run: dotnet pack --property:ContinuousIntegrationBuild=true --version-suffix ".${{ env.BUILD_NUMBER }}" --configuration Release --output ${{ env.NuGetDirectory }} # Publish the NuGet package as an artifact, so they can be used in the following jobs - uses: actions/upload-artifact@v3 @@ -80,10 +75,7 @@ jobs: # If some rules are not applicable, you can disable them # using the --excluded-rules or --excluded-rule-ids option - name: Validate package - run: | - meziantou.validate-nuget-package ' - (Get-ChildItem "${{ env.NuGetDirectory }}/*.nupkg") ' - --excluded-rules LicenseMustBeSet + run: meziantou.validate-nuget-package (Get-ChildItem "${{ env.NuGetDirectory }}/*.nupkg") --excluded-rules LicenseMustBeSet run_test: From 3abbaf604ae1fa8e7a6ccb2bc6082a10ca58bec4 Mon Sep 17 00:00:00 2001 From: Maxim Liven Date: Wed, 20 Nov 2024 09:50:20 +0300 Subject: [PATCH 26/31] docs: add xml docs into nuget --- Directory.Build.props | 9 ++++++++- sources/McQuery.Net/Data/StatusBase.cs | 13 ++++++++++++- sources/McQuery.Net/Exceptions/ExpiredException.cs | 8 ++++++++ sources/McQuery.Net/IMcQueryClientFactory.cs | 7 +++++++ .../Helpers/CancellationTokenTimeoutEnricher.cs | 2 +- sources/McQuery.Net/McQuery.Net.csproj | 6 ++---- sources/McQuery.Net/McQueryClient.cs | 1 + sources/McQuery.Net/McQueryClientFactory.cs | 8 ++++++++ 8 files changed, 47 insertions(+), 7 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index c842840..a3092c3 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -8,8 +8,10 @@ false false true + embedded true snupkg + true @@ -18,7 +20,7 @@ - MaxLevs + Maxim (MaxLevs) Liven minecraft, query, client icon.png README.md @@ -30,6 +32,11 @@ git + + $(ProjectName) + $(ProjectName) + + ../.. diff --git a/sources/McQuery.Net/Data/StatusBase.cs b/sources/McQuery.Net/Data/StatusBase.cs index 3455608..f8a0b42 100644 --- a/sources/McQuery.Net/Data/StatusBase.cs +++ b/sources/McQuery.Net/Data/StatusBase.cs @@ -2,8 +2,19 @@ namespace McQuery.Net.Data; +/// +/// Represents a basic status response. +/// +/// +/// Message of the day. +/// Type of the game. +/// Name of a map. +/// Current number of players. +/// Maximum number of players what is allowed to enter. +/// Port to connect. +/// Ip to connect. [PublicAPI] -public record StatusBase( +public abstract record StatusBase( string Motd, string GameType, string Map, diff --git a/sources/McQuery.Net/Exceptions/ExpiredException.cs b/sources/McQuery.Net/Exceptions/ExpiredException.cs index 95ade33..9a8fc1f 100644 --- a/sources/McQuery.Net/Exceptions/ExpiredException.cs +++ b/sources/McQuery.Net/Exceptions/ExpiredException.cs @@ -2,6 +2,9 @@ namespace McQuery.Net.Exceptions; +/// +/// Something was expired. +/// [PublicAPI] public class ExpiredException : ArgumentException { @@ -10,6 +13,11 @@ internal ExpiredException(IExpirable expirable) { } + /// + /// Helper method to throw new exception form . + /// + /// Something that can be expired. + /// Something was exprired. internal static void ThrowIfExpired(IExpirable expirable) { if (expirable.IsExpired) throw new ExpiredException(expirable); diff --git a/sources/McQuery.Net/IMcQueryClientFactory.cs b/sources/McQuery.Net/IMcQueryClientFactory.cs index 3e53f3d..837d020 100644 --- a/sources/McQuery.Net/IMcQueryClientFactory.cs +++ b/sources/McQuery.Net/IMcQueryClientFactory.cs @@ -1,7 +1,14 @@ namespace McQuery.Net; +/// +/// Factory to create instances of . +/// [PublicAPI] public interface IMcQueryClientFactory { + /// + /// Create instance of . + /// + /// Instance of . IMcQueryClient Get(); } diff --git a/sources/McQuery.Net/Internal/Helpers/CancellationTokenTimeoutEnricher.cs b/sources/McQuery.Net/Internal/Helpers/CancellationTokenTimeoutEnricher.cs index 0d87c95..6c58c84 100644 --- a/sources/McQuery.Net/Internal/Helpers/CancellationTokenTimeoutEnricher.cs +++ b/sources/McQuery.Net/Internal/Helpers/CancellationTokenTimeoutEnricher.cs @@ -2,7 +2,7 @@ namespace McQuery.Net.Internal.Helpers; -public static class CancellationTokenTimeoutEnrichHelper +internal static class CancellationTokenTimeoutEnrichHelper { public static CancellationTokenSourceWithTimeout ToSourceWithTimeout(this CancellationToken token, TimeSpan timeout) => CancellationTokenSourceWithTimeout.Create(token, timeout); diff --git a/sources/McQuery.Net/McQuery.Net.csproj b/sources/McQuery.Net/McQuery.Net.csproj index f27d73f..010e7ba 100644 --- a/sources/McQuery.Net/McQuery.Net.csproj +++ b/sources/McQuery.Net/McQuery.Net.csproj @@ -1,15 +1,13 @@  + true + McQuery.Net Library for .Net which implements Minecraft Query protocol. You can use it for getting statuses of a Minecraft server. - - true - - diff --git a/sources/McQuery.Net/McQueryClient.cs b/sources/McQuery.Net/McQueryClient.cs index af028b9..21746ba 100644 --- a/sources/McQuery.Net/McQueryClient.cs +++ b/sources/McQuery.Net/McQueryClient.cs @@ -133,6 +133,7 @@ async Task ExecuteRequestConcurrentlyAsync() private bool _isDisposed; + /// public void Dispose() { if (_isDisposed) return; diff --git a/sources/McQuery.Net/McQueryClientFactory.cs b/sources/McQuery.Net/McQueryClientFactory.cs index 2498d3c..a5d624e 100644 --- a/sources/McQuery.Net/McQueryClientFactory.cs +++ b/sources/McQuery.Net/McQueryClientFactory.cs @@ -6,6 +6,9 @@ namespace McQuery.Net; +/// +/// Implementation of . +/// [UsedImplicitly] public class McQueryClientFactory : IMcQueryClientFactory { @@ -13,6 +16,10 @@ public class McQueryClientFactory : IMcQueryClientFactory private readonly Lazy _sessionIdProvider; private readonly Lazy _client; + /// + /// .ctor. + /// + /// . public McQueryClientFactory(ILoggerFactory? loggerFactory = null) { _loggerFactory = loggerFactory; @@ -20,6 +27,7 @@ public McQueryClientFactory(ILoggerFactory? loggerFactory = null) _client = new Lazy(AcquireClient, isThreadSafe: true); } + /// public IMcQueryClient Get() => _client.Value; private IMcQueryClient AcquireClient() From 6a2d0819e752af1e7c5e48760cb93fbf67f69d33 Mon Sep 17 00:00:00 2001 From: Maxim Liven Date: Wed, 20 Nov 2024 09:52:19 +0300 Subject: [PATCH 27/31] fix: update checkout --- .github/workflows/nuget_publish.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/nuget_publish.yaml b/.github/workflows/nuget_publish.yaml index 1f27b89..b4f4237 100644 --- a/.github/workflows/nuget_publish.yaml +++ b/.github/workflows/nuget_publish.yaml @@ -26,7 +26,7 @@ jobs: create_nuget: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 0 # Get all history to allow automatic versioning using MinVer @@ -81,7 +81,7 @@ jobs: run_test: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Setup .NET uses: actions/setup-dotnet@v4 with: From ff8eb2ed620978e36063cb8dcc9d2fdb76401f5a Mon Sep 17 00:00:00 2001 From: Maxim Liven Date: Wed, 20 Nov 2024 09:54:20 +0300 Subject: [PATCH 28/31] fix: update download artifacts version --- .github/workflows/nuget_publish.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/nuget_publish.yaml b/.github/workflows/nuget_publish.yaml index b4f4237..5966552 100644 --- a/.github/workflows/nuget_publish.yaml +++ b/.github/workflows/nuget_publish.yaml @@ -42,7 +42,7 @@ jobs: run: dotnet pack --property:ContinuousIntegrationBuild=true --version-suffix ".${{ env.BUILD_NUMBER }}" --configuration Release --output ${{ env.NuGetDirectory }} # Publish the NuGet package as an artifact, so they can be used in the following jobs - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 with: name: nuget if-no-files-found: error @@ -62,7 +62,7 @@ jobs: dotnet-version: ${{ env.DOTNET_TARGET_VERSION }} # Download the NuGet package created in the previous job - - uses: actions/download-artifact@v3 + - uses: actions/download-artifact@v4 with: name: nuget path: ${{ env.NuGetDirectory }} From b8a4d982dba557b7974685ff79fe12664e917e56 Mon Sep 17 00:00:00 2001 From: Maxim Liven Date: Wed, 20 Nov 2024 12:07:39 +0300 Subject: [PATCH 29/31] docs: update readme --- README.md | 38 ++++++++++++++++++++++---------------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 1812cd9..e43f3f9 100644 --- a/README.md +++ b/README.md @@ -1,28 +1,34 @@ # McQuery.Net + Library for .Net which implements Minecraft Query protocol. You can use it for getting statuses of a Minecraft server. # Example of using ```cs -static async Task DoSomething(IEnumerable mcServersEndPoints) -{ - McQueryService service = new(5, 5000, 500, 1000); +IMcQueryClientFactory factory = new McQueryClientFactory(); +using var client = factory.Get(); - List servers = mcServersEndPoints.Select(service.RegistrateServer).ToList(); +async Task ExecuteQueries(IReadOnlyCollection endpoints, CancellationToken cancellationToken = default) +{ + var queryTasks = endpoints.SelectMany( + endpoint => + [ + GetBasicStatusAndPrint(endpoint, cancellationToken), + GetFullStatusAndPrint(endpoint, cancellationToken) + ], + (_, task) => task + ).ToArray(); - List> requests = new(); - foreach (Server server in servers) - { - requests.Add(service.GetBasicStatusCommon(server)); - requests.Add(service.GetFullStatusCommon(server)); - } + await Task.WhenAll(queryTasks); +} - Task.WaitAll(requests.ToArray()); +async Task GetBasicStatusAndPrint(IPEndPoint endpoint, CancellationToken cancellationToken = default) +{ + Console.WriteLine(await client.GetBasicStatusAsync(endpoint, cancellationToken)); +} - foreach (Task request in requests) - { - IResponse response = await request; - Console.WriteLine(response.ToString() + "\n"); - } +async Task GetFullStatusAndPrint(IPEndPoint endpoint, CancellationToken cancellationToken = default) +{ + Console.WriteLine(await client.GetFullStatusAsync(endpoint, cancellationToken)); } ``` From 458b3d85cf3359610c1aacb2d5e15cf1fb84f045 Mon Sep 17 00:00:00 2001 From: Maxim Liven Date: Wed, 20 Nov 2024 13:09:59 +0300 Subject: [PATCH 30/31] fix: temp versioning --- .github/workflows/nuget_publish.yaml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/nuget_publish.yaml b/.github/workflows/nuget_publish.yaml index 5966552..c1877ed 100644 --- a/.github/workflows/nuget_publish.yaml +++ b/.github/workflows/nuget_publish.yaml @@ -39,7 +39,13 @@ jobs: # Create the NuGet package in the folder from the environment variable NuGetDirectory - name: Build packages - run: dotnet pack --property:ContinuousIntegrationBuild=true --version-suffix ".${{ env.BUILD_NUMBER }}" --configuration Release --output ${{ env.NuGetDirectory }} + run: dotnet pack --property:ContinuousIntegrationBuild=true --version-suffix ".${{ env.BUILD_NUMBER }}-dev" --configuration Release --output ${{ env.NuGetDirectory }} + if: github.event_name != 'release' + + # Create the NuGet package in the folder from the environment variable NuGetDirectory + - name: Build packages + run: dotnet pack --property:ContinuousIntegrationBuild=true --version-suffix "-beta" --configuration Release --output ${{ env.NuGetDirectory }} + if: github.event_name == 'release' # Publish the NuGet package as an artifact, so they can be used in the following jobs - uses: actions/upload-artifact@v4 From aed9e9d61d38cddbe0d872add84884210d145d00 Mon Sep 17 00:00:00 2001 From: Maxim Liven Date: Wed, 20 Nov 2024 13:14:39 +0300 Subject: [PATCH 31/31] fix: publishing --- .github/workflows/nuget_publish.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/nuget_publish.yaml b/.github/workflows/nuget_publish.yaml index c1877ed..b4f111b 100644 --- a/.github/workflows/nuget_publish.yaml +++ b/.github/workflows/nuget_publish.yaml @@ -104,7 +104,7 @@ jobs: needs: [ validate_nuget, run_test ] steps: # Download the NuGet package created in the previous job - - uses: actions/download-artifact@v3 + - uses: actions/download-artifact@v4 with: name: nuget path: ${{ env.NuGetDirectory }}