Skip to content

Commit 4212852

Browse files
committed
Complete add GetPlayers and GetRules functions.
1 parent 9c6acf6 commit 4212852

4 files changed

Lines changed: 198 additions & 47 deletions

File tree

SteamQueryNet/SteamQueryNet.Tests/ServerQueryTests.cs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ namespace SteamQueryNet.Tests
66
{
77
public class ServerQueryTests
88
{
9-
private const string IP_ADDRESS = "54.37.111.216";
10-
private const string HOST_NAME = "surf.wasdzone.com";
9+
private const string IP_ADDRESS = "127.0.0.1";
10+
private const string HOST_NAME = "localhost";
1111
private const int PORT = 27015;
1212

1313
[Theory]
@@ -16,7 +16,6 @@ public class ServerQueryTests
1616
public void ShouldInitializeWithProperHost(string host)
1717
{
1818
var squery = new ServerQuery(host, PORT);
19-
squery.GetServerInfo();
2019
}
2120

2221
[Theory]

SteamQueryNet/SteamQueryNet/Models/PacketHeaders.cs

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,4 @@ public sealed class RequestHeaders
88

99
public const byte A2S_RULES = 0x56;
1010
}
11-
12-
public sealed class ResponseHeaders
13-
{
14-
public const byte S2A_INFO = 0x49;
15-
16-
public const byte S2A_CHALLENGE = 0x41;
17-
18-
public const byte S2A_PLAYER = 0x44;
19-
20-
public const byte S2A_RULES = 0x45;
21-
}
2211
}

SteamQueryNet/SteamQueryNet/Models/Player.cs

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
using SteamQueryNet.Models.TheShip;
2+
using SteamQueryNet.Utils;
3+
using System;
24

35
namespace SteamQueryNet.Models
46
{
@@ -17,16 +19,34 @@ public sealed class Player
1719
/// <summary>
1820
/// Player's score (usually "frags" or "kills".)
1921
/// </summary>
20-
public long Score { get; set; }
22+
public int Score { get; set; }
2123

2224
/// <summary>
2325
/// Time (in seconds) player has been connected to the server.
2426
/// </summary>
2527
public float Duration { get; set; }
2628

29+
/// <summary>
30+
/// Total time as Hours:Minutes:Seconds format.
31+
/// </summary>
32+
[NotParsable]
33+
public string TotalDurationAsString
34+
{
35+
get
36+
{
37+
TimeSpan totalSpan = TimeSpan.FromSeconds(Duration);
38+
string parsedHours = totalSpan.Hours >= 10 ? totalSpan.Hours.ToString() : $"0{totalSpan.Hours}";
39+
string parsedMinutes = totalSpan.Minutes >= 10 ? totalSpan.Minutes.ToString() : $"0{totalSpan.Minutes}";
40+
string parsedSeconds = totalSpan.Seconds >= 10 ? totalSpan.Seconds.ToString() : $"0{totalSpan.Seconds}";
41+
return $"{parsedHours}:{parsedMinutes}:{parsedSeconds}";
42+
}
43+
}
44+
2745
/// <summary>
2846
/// The Ship additional player info.
2947
/// </summary>
48+
/// Warning: this property information is not supported by SteamQueryNet yet.
49+
[ParseCustom]
3050
public ShipPlayerDetails ShipPlayerDetails { get; set; }
3151
}
3252
}

SteamQueryNet/SteamQueryNet/ServerQuery.cs

Lines changed: 175 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,43 @@ namespace SteamQueryNet
1515
// This is not really required but imma be a good guy and create this for them people that wants to mock the ServerQuery.
1616
public interface IServerQuery
1717
{
18+
/// <summary>
19+
/// Renews the server challenge code of the ServerQuery instance in order to be able to execute further operations.
20+
/// </summary>
1821
void RenewChallenge();
22+
23+
/// <summary>
24+
/// Requests and serializes the server information.
25+
/// </summary>
26+
/// <returns>Serialized ServerInfo instance.</returns>
1927
ServerInfo GetServerInfo();
28+
29+
/// <summary>
30+
/// Requests and serializes the list of player information.
31+
/// </summary>
32+
/// <returns>Serialized list of Player instances.</returns>
33+
List<Player> GetPlayers();
34+
35+
/// <summary>
36+
/// Requests and serializes the list of rules defined by the server.
37+
/// Warning: CS:GO Rules reply is broken since update CSGO 1.32.3.0 (Feb 21, 2014).
38+
/// Before the update rules got truncated when exceeding MTU, after the update rules reply is not sent at all.
39+
/// </summary>
40+
/// <returns>Serialized list of Rule instances.</returns>
41+
List<Rule> GetRules();
2042
}
2143

2244
public class ServerQuery : IServerQuery, IDisposable
2345
{
46+
private const int RESPONSE_HEADER_COUNT = 6;
47+
private const int RESPONSE_CODE_INDEX = 5;
48+
2449
private readonly int _port;
2550
private readonly UdpClient _client = new UdpClient(new IPEndPoint(IPAddress.Any, 0));
2651
private IPEndPoint _ipEndpoint;
2752

53+
private int _currentChallenge;
54+
2855
/// <summary>
2956
/// Reflects the udp client connection state.
3057
/// </summary>
@@ -93,32 +120,105 @@ public ServerQuery(string serverAddress, int port)
93120
_client.Connect(_ipEndpoint);
94121
}
95122

123+
/// <inheritdoc/>
124+
public ServerInfo GetServerInfo()
125+
{
126+
const string requestPayload = "Source Engine Query\0";
127+
var sInfo = new ServerInfo
128+
{
129+
Ping = new Ping().Send(_ipEndpoint.Address).RoundtripTime
130+
};
131+
132+
byte[] response = SendRequest(RequestHeaders.A2S_INFO, Encoding.UTF8.GetBytes(requestPayload));
133+
if (response.Length > 0)
134+
{
135+
ExtractData(sInfo, response, nameof(sInfo.EDF), true);
136+
}
137+
138+
return sInfo;
139+
}
140+
141+
/// <inheritdoc/>
142+
public void RenewChallenge()
143+
{
144+
byte[] response = SendRequest(RequestHeaders.A2S_PLAYER, BitConverter.GetBytes(-1));
145+
if (response.Length > 0)
146+
{
147+
_currentChallenge = BitConverter.ToInt32(response.Skip(5).Take(sizeof(int)).ToArray(), 0);
148+
}
149+
}
150+
151+
/// <inheritdoc/>
152+
public List<Player> GetPlayers()
153+
{
154+
if (_currentChallenge == 0)
155+
{
156+
RenewChallenge();
157+
}
158+
159+
byte[] response = SendRequest(RequestHeaders.A2S_PLAYER, BitConverter.GetBytes(_currentChallenge));
160+
if (response.Length > 0)
161+
{
162+
return ExtractListData<Player>(response);
163+
}
164+
else
165+
{
166+
throw new InvalidOperationException("Server did not response the query");
167+
}
168+
}
169+
170+
/// <inheritdoc/>
171+
public List<Rule> GetRules()
172+
{
173+
if (_currentChallenge == 0)
174+
{
175+
RenewChallenge();
176+
}
177+
178+
byte[] response = SendRequest(RequestHeaders.A2S_RULES, BitConverter.GetBytes(_currentChallenge));
179+
if (response.Length > 0)
180+
{
181+
var rls = ExtractListData<Rule>(response);
182+
return ExtractListData<Rule>(response);
183+
}
184+
else
185+
{
186+
throw new InvalidOperationException("Server did not response the query");
187+
}
188+
}
189+
190+
/// <summary>
191+
/// Disposes the object and its disposables.
192+
/// </summary>
96193
public void Dispose()
97194
{
98195
_client.Close();
99196
_client.Dispose();
100197
}
101198

102-
public ServerInfo GetServerInfo()
199+
private List<TObject> ExtractListData<TObject>(byte[] rawSource)
200+
where TObject : class
103201
{
104-
var sInfo = new ServerInfo();
105-
const string requestPayload = "Source Engine Query\0";
106-
try
107-
{
108-
sInfo.Ping = new Ping().Send(_ipEndpoint.Address).RoundtripTime;
109-
byte[] response = SendRequest(RequestHeaders.A2S_INFO, Encoding.UTF8.GetBytes(requestPayload));
110-
if (response.Length > 0)
111-
{
112-
ExtractData(sInfo, response, nameof(sInfo.EDF));
113-
}
114-
}
115-
catch (Exception)
202+
// Create a list to contain the serialized data.
203+
var objectList = new List<TObject>();
204+
205+
// Skip the response headers.
206+
IEnumerable<byte> dataSource = rawSource.Skip(RESPONSE_HEADER_COUNT);
207+
208+
// Iterate amount of times that the server said.
209+
for (byte i = 0; i < rawSource[RESPONSE_CODE_INDEX]; i++)
116210
{
117-
// TODO: Log it.
118-
throw;
211+
// Activate a new instance of the object.
212+
var objectInstance = Activator.CreateInstance<TObject>();
213+
214+
// Extract the data.
215+
dataSource = ExtractData(objectInstance, dataSource.ToArray());
216+
217+
// Add it into the list.
218+
objectList.Add(objectInstance);
119219
}
120220

121-
return sInfo;
221+
return objectList;
122222
}
123223

124224
private byte[] SendRequest(byte requestHeader, byte[] payload = null)
@@ -128,56 +228,99 @@ private byte[] SendRequest(byte requestHeader, byte[] payload = null)
128228
return _client.Receive(ref _ipEndpoint);
129229
}
130230

131-
public void RenewChallenge()
132-
{
133-
throw new NotImplementedException();
134-
}
135-
136231
private byte[] BuildRequest(byte headerCode, byte[] extraParams = null)
137232
{
233+
/* All requests consists 4 FF's and a header code to execute the request.
234+
* Check here: https://developer.valvesoftware.com/wiki/Server_queries#Protocol for further information about the protocol. */
138235
var request = new byte[] { 0xFF, 0xFF, 0xFF, 0xFF, headerCode };
236+
237+
// If we have any extra payload, concatenate those into our requestHeaders and return;
139238
return extraParams != null ? request.Concat(extraParams).ToArray() : request;
140239
}
141240

142-
private void ExtractData<TObject>(TObject objectRef, byte[] dataSource, string edfPropName = "")
241+
private IEnumerable<byte> ExtractData<TObject>(TObject objectRef, byte[] dataSource, string edfPropName = "", bool stripHeaders = false)
143242
where TObject : class
144243
{
145244
IEnumerable<byte> takenBytes = new List<byte>();
146-
IEnumerable<byte> strippedSource = dataSource.Skip(5);
245+
246+
// We can be a good guy and ask for any extra jobs :)
247+
IEnumerable<byte> enumerableSource = stripHeaders ? dataSource.Skip(RESPONSE_HEADER_COUNT) : dataSource;
248+
249+
// We get every property that does not contain ParseCustom and NotParsable attributes on them to iterate through all and parse/assign their values.
147250
IEnumerable<PropertyInfo> propsOfObject = typeof(TObject).GetProperties()
148251
.Where(x => x.CustomAttributes.Count(y => y.AttributeType == typeof(ParseCustomAttribute)
149252
|| y.AttributeType == typeof(NotParsableAttribute)) == 0);
150253

151254
foreach (PropertyInfo property in propsOfObject)
152255
{
153-
CustomAttributeData edfInfo = property.CustomAttributes.FirstOrDefault(x => x.AttributeType == typeof(EDFAttribute));
154-
if (edfInfo != null)
256+
/* Check for EDF property name, if it was provided then it mean that we have EDF properties in the model.
257+
* You can check here: https://developer.valvesoftware.com/wiki/Server_queries#A2S_INFO to get more info about EDF's. */
258+
if (!string.IsNullOrEmpty(edfPropName))
155259
{
156-
byte edfValue = (byte)typeof(TObject).GetProperty(edfPropName).GetValue(objectRef);
157-
byte edfPropertyConditionValue = (byte)edfInfo.ConstructorArguments[0].Value;
158-
if ((edfValue & edfPropertyConditionValue) <= 0) { continue; }
260+
// Does the property have an EDFAttribute assigned ?
261+
CustomAttributeData edfInfo = property.CustomAttributes.FirstOrDefault(x => x.AttributeType == typeof(EDFAttribute));
262+
if (edfInfo != null)
263+
{
264+
// Get the EDF value that was returned by the server.
265+
byte edfValue = (byte)typeof(TObject).GetProperty(edfPropName).GetValue(objectRef);
266+
267+
// Get the EDF condition value that was provided in the model.
268+
byte edfPropertyConditionValue = (byte)edfInfo.ConstructorArguments[0].Value;
269+
270+
// Continue if the condition does not pass because it means that the server did not include any information about this property.
271+
if ((edfValue & edfPropertyConditionValue) <= 0) { continue; }
272+
}
159273
}
160274

275+
/* Basic explanation of what is going of from here;
276+
* Get the type of the property and get amount of bytes of its size from the response array,
277+
* Convert the parsed value to its type and assign it.
278+
*/
279+
280+
/* We have to handle strings seperately since their size is unknown and they are also null terminated.
281+
* Check here: https://developer.valvesoftware.com/wiki/String for further information about Strings in the protocol.
282+
*/
161283
if (property.PropertyType == typeof(string))
162284
{
163-
takenBytes = strippedSource.TakeWhile(x => x != 0);
285+
// Take till the termination.
286+
takenBytes = enumerableSource.TakeWhile(x => x != 0);
287+
288+
// Parse it into a string.
164289
property.SetValue(objectRef, Encoding.UTF8.GetString(takenBytes.ToArray()));
165-
strippedSource = strippedSource.Skip(takenBytes.Count() + 1); // +1 for null termination
290+
291+
// Update the source by skipping the amount of bytes taken from the source and + 1 for termination byte.
292+
enumerableSource = enumerableSource.Skip(takenBytes.Count() + 1);
166293
}
167294
else
168295
{
296+
// Is the property an Enum ? if yes we should be getting the underlying type since it might differ.
169297
Type typeOfProperty = property.PropertyType.IsEnum ? property.PropertyType.GetEnumUnderlyingType() : property.PropertyType;
170-
(object result, int size) = ExtractMarshalType(strippedSource, typeOfProperty);
298+
299+
// Extract the value and the size from the source.
300+
(object result, int size) = ExtractMarshalType(enumerableSource, typeOfProperty);
301+
302+
/* If the property is an enum we should parse it first then assign its value,
303+
* if not we can just give it to SetValue since it was converted by ExtractMarshalType already.*/
171304
property.SetValue(objectRef, property.PropertyType.IsEnum ? Enum.Parse(property.PropertyType, result.ToString()) : result);
172-
strippedSource = strippedSource.Skip(size);
305+
306+
// Update the source by skipping the amount of bytes taken from the source.
307+
enumerableSource = enumerableSource.Skip(size);
173308
}
174309
}
310+
311+
// We return the last state of the processed source.
312+
return enumerableSource;
175313
}
176314

177315
private (object, int) ExtractMarshalType(IEnumerable<byte> source, Type type)
178316
{
317+
// Get the size of the given type.
179318
int sizeOfType = Marshal.SizeOf(type);
319+
320+
// Take amount of bytes from the source array.
180321
IEnumerable<byte> takenBytes = source.Take(sizeOfType);
322+
323+
// We actually need to go into an unsafe block here since as far as i know, this is the only way to convert a byte[] source into its given type on runtime.
181324
unsafe
182325
{
183326
fixed (byte* sourcePtr = takenBytes.ToArray())

0 commit comments

Comments
 (0)