@@ -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