From d92a7f79017132c3a6b3bc879a344d47015f2244 Mon Sep 17 00:00:00 2001 From: SuperKod <79586895+terenP@users.noreply.github.com> Date: Wed, 18 Feb 2026 22:04:29 +0100 Subject: [PATCH 1/3] Add files via upload --- CSharp/MineStat/MineStat.cs | 206 ++++++++++++++++++++++++++++++++++-- 1 file changed, 200 insertions(+), 6 deletions(-) diff --git a/CSharp/MineStat/MineStat.cs b/CSharp/MineStat/MineStat.cs index 2c889b5..d2ce59c 100644 --- a/CSharp/MineStat/MineStat.cs +++ b/CSharp/MineStat/MineStat.cs @@ -30,6 +30,7 @@ using System.Xml; using System.Xml.Linq; using System.Xml.XPath; +using System.Net; namespace MineStatLib { @@ -182,6 +183,9 @@ public MineStat(string address, ushort port, int timeout = DefaultTimeout, SlpPr case SlpProtocol.Bedrock_Raknet: ConnectionStatus = RequestWithRaknetProtocol(); break; + case SlpProtocol.Ut3Gs4Query: + ConnectionStatus = RequestWithUt3Gs4QueryProtocol(); + break; case SlpProtocol.Automatic: break; default: @@ -201,15 +205,18 @@ public MineStat(string address, ushort port, int timeout = DefaultTimeout, SlpPr // For more information, see https://github.com/FragLand/minestat/issues/70 // // 1.: Raknet (Bedrock) - // 2.: Legacy (1.4, 1.5) - // 3.: Beta (b1.8-rel1.3) - // 4.: Extended Legacy (1.6) - // 5.: JSON (1.7+) + // 2.: UT3/GS4 Query + // 3.: Legacy (1.4, 1.5) + // 4.: Beta (b1.8-rel1.3) + // 5.: Extended Legacy (1.6) + // 6.: JSON (1.7+) ConnectionStatus = RequestWithRaknetProtocol(); if (ConnectionStatus == ConnStatus.Connfail || ConnectionStatus == ConnStatus.Success) return; + ConnectionStatus = RequestWithUt3Gs4QueryProtocol(); + ConnectionStatus = RequestWrapper(RequestWithLegacyProtocol); ConnStatus result; @@ -780,8 +787,183 @@ private ConnStatus ParseBetaProtocol(byte[] rawPayload) Version = "<= 1.3"; return ConnStatus.Success; + } + + /// + /// Requests the server data with the UT3/GS4 Query protocol. + /// + /// ConnStatus - See for possible values + /// + public ConnStatus RequestWithUt3Gs4QueryProtocol() + { + var sock = new UdpClient(); + sock.Client.ReceiveTimeout = Timeout * 1000; + sock.Client.SendTimeout = Timeout * 1000; + + var stopWatch = new Stopwatch(); + stopWatch.Start(); + + try + { + sock.Connect(Address, Port); + } + catch (SocketException) + { + return ConnStatus.Connfail; + } + + stopWatch.Stop(); + Latency = stopWatch.ElapsedMilliseconds; + + + byte[] statResponse; + try + { + // Get Challenge Token + var challengeRequest = new byte[] { 0xFE, 0xFD, 0x09, 0x00, 0x00, 0x00, 0x00}; + sock.Send(challengeRequest, challengeRequest.Length); + + IPEndPoint remoteEP = new IPEndPoint(IPAddress.Any, Port); + byte[] challengeResponse = sock.Receive(ref remoteEP); + + if (challengeResponse.Length < 11) return ConnStatus.Unknown; + + //(Big-Endian) + byte[] tokenBytes = new byte[4]; + Array.Copy(challengeResponse, 7, tokenBytes, 0, 4); + if (BitConverter.IsLittleEndian) + Array.Reverse(tokenBytes); + int token = BitConverter.ToInt32(tokenBytes, 0); + + byte[] statRequest = new byte[11]; + statRequest[0] = 0xFE; statRequest[1] = 0xFD; statRequest[2] = 0x00; + + // Token jako Big-Endian + tokenBytes = BitConverter.GetBytes(token); + if (BitConverter.IsLittleEndian) + Array.Reverse(tokenBytes); + Array.Copy(tokenBytes, 0, statRequest, 7, 4); + + sock.Send(statRequest, statRequest.Length); + + // --- 3. ODBIERZ STAT RESPONSE --- + statResponse = sock.Receive(ref remoteEP); + + } + catch + { + return ConnStatus.Unknown; + } + finally + { + sock.Close(); + } + + return ParseUt3Gs4Protocol(statResponse); } - + + /// + /// Internal helper method for parsing the UT3/GS4 Query protocol payload. + /// + /// The raw payload, without packet length and -id + /// ConnStatus - See for possible values + private ConnStatus ParseUT3GS4Protocol(byte[] rawPayload) + { + try + { + int pos = 5; + + var data = new Dictionary(); + + while (pos < rawPayload.Length && rawPayload[pos] != 0) + { + // Read Key + int keyStart = pos; + while (pos < rawPayload.Length && rawPayload[pos] != 0) + pos++; + + if (pos >= rawPayload.Length) break; + + string key = Encoding.UTF8.GetString(rawPayload, keyStart, pos - keyStart); + pos++; // We omit null + + if (pos >= rawPayload.Length) break; + + // Read value + int valStart = pos; + while (pos < rawPayload.Length && rawPayload[pos] != 0) + pos++; + + if (pos >= rawPayload.Length) break; + + string value = Encoding.UTF8.GetString(rawPayload, valStart, pos - valStart); + pos++; // We omit null + + data[key] = value; + } + + // Check if we have the minimum data + if (!data.ContainsKey("hostname") && !data.ContainsKey("numplayers")) + { + return ConnStatus.Unknown; + } + + + // MOTD + if (data.TryGetValue("hostname", out var hostname)) + { + Motd = hostname; + Stripped_Motd = strip_motd_formatting(hostname); + } + else + { + Motd = "Unknown"; + Stripped_Motd = "Unknown"; + } + + // Version + if (data.TryGetValue("version", out var version)) + { + Version = version; + } + else + { + Version = "Unknown"; + } + + // Players count + if (data.TryGetValue("numplayers", out var num) && int.TryParse(num, out int current)) + { + CurrentPlayersInt = current; + } + else + { + CurrentPlayersInt = 0; + } + + if (data.TryGetValue("maxplayers", out var max) && int.TryParse(max, out int maximum)) + { + MaximumPlayersInt = maximum; + } + else + { + MaximumPlayersInt = 0; + } + + // --- SET STATUS --- + ServerUp = true; + Protocol = SlpProtocol.Ut3Gs4Query; + + return ConnStatus.Success; + } + catch (Exception) + { + return ConnStatus.Unknown; + } + + + } + /// /// Internal helper method for connecting to a remote host and setting timeouts. /// @@ -1159,7 +1341,19 @@ public enum SlpProtocol /// It contains very few details, no server version info, only MOTD, max- and online player counts.
/// Available since Minecraft Beta 1.8 /// - Beta, + Beta, + + /// + /// Requests server data using the UT3/GS4 Query protocol (GameSpy 4). + /// UDP-based protocol used by Minecraft Beta 1.8 through 1.6.x. + /// + /// /// Protocol flow: + /// 1. Send challenge request [FE FD 09 00 00 00 00] + /// 2. Receive token (4-byte Big-Endian integer) + /// 3. Send stat request with token [FE FD 00 session token] + /// 4. Parse null-terminated key-value pairs from response + /// + Ut3Gs4Query, /// /// Not a protocol. Used for setting the default, automatic protocol detection. From 9ef230db77251d23561439b4df1d07096af91628 Mon Sep 17 00:00:00 2001 From: SuperKod <79586895+terenP@users.noreply.github.com> Date: Thu, 19 Feb 2026 12:31:35 +0100 Subject: [PATCH 2/3] Add files via upload --- CSharp/MineStat/MineStat.cs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/CSharp/MineStat/MineStat.cs b/CSharp/MineStat/MineStat.cs index d2ce59c..2022477 100644 --- a/CSharp/MineStat/MineStat.cs +++ b/CSharp/MineStat/MineStat.cs @@ -815,7 +815,6 @@ public ConnStatus RequestWithUt3Gs4QueryProtocol() stopWatch.Stop(); Latency = stopWatch.ElapsedMilliseconds; - byte[] statResponse; try { @@ -848,7 +847,6 @@ public ConnStatus RequestWithUt3Gs4QueryProtocol() // --- 3. ODBIERZ STAT RESPONSE --- statResponse = sock.Receive(ref remoteEP); - } catch { @@ -859,7 +857,7 @@ public ConnStatus RequestWithUt3Gs4QueryProtocol() sock.Close(); } - return ParseUt3Gs4Protocol(statResponse); + return ParseUT3GS4Protocol(statResponse); } /// @@ -908,7 +906,6 @@ private ConnStatus ParseUT3GS4Protocol(byte[] rawPayload) return ConnStatus.Unknown; } - // MOTD if (data.TryGetValue("hostname", out var hostname)) { @@ -960,8 +957,6 @@ private ConnStatus ParseUT3GS4Protocol(byte[] rawPayload) { return ConnStatus.Unknown; } - - } /// From f205135ad62813d4e41ed83e35a6147621eb864f Mon Sep 17 00:00:00 2001 From: SuperKod <79586895+terenP@users.noreply.github.com> Date: Thu, 19 Feb 2026 12:39:30 +0100 Subject: [PATCH 3/3] Add files via upload --- CSharp/MineStat/MineStat.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CSharp/MineStat/MineStat.cs b/CSharp/MineStat/MineStat.cs index 2022477..bb226bb 100644 --- a/CSharp/MineStat/MineStat.cs +++ b/CSharp/MineStat/MineStat.cs @@ -837,7 +837,7 @@ public ConnStatus RequestWithUt3Gs4QueryProtocol() byte[] statRequest = new byte[11]; statRequest[0] = 0xFE; statRequest[1] = 0xFD; statRequest[2] = 0x00; - // Token jako Big-Endian + // Token Big-Endian tokenBytes = BitConverter.GetBytes(token); if (BitConverter.IsLittleEndian) Array.Reverse(tokenBytes); @@ -845,10 +845,10 @@ public ConnStatus RequestWithUt3Gs4QueryProtocol() sock.Send(statRequest, statRequest.Length); - // --- 3. ODBIERZ STAT RESPONSE --- + // --- 3. RECEIVE STAT RESPONSE --- statResponse = sock.Receive(ref remoteEP); } - catch + catch (Exception) { return ConnStatus.Unknown; }