Module: GameServer

Includes:
Server
Included in:
GoldSrcServer, SourceServer
Defined in:
lib/steam/servers/game_server.rb

Overview

This module is included by classes representing different game server implementations and provides the basic functionality to communicate with them using the common query protocol

Author:

  • Sebastian Staudt

Instance Attribute Summary

Attributes included from Server

#host_names, #ip_addresses

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Server

#disconnect, #rotate_ip

Class Method Details

.player_status_attributes(status_header) ⇒ Array<Symbol>

Parses the player attribute names supplied by ‘rcon status`

Parameters:

  • status_header (String)

    The header line provided by ‘rcon status`

Returns:

  • (Array<Symbol>)

    Split player attribute names

See Also:



33
34
35
36
37
38
39
40
41
42
43
44
# File 'lib/steam/servers/game_server.rb', line 33

def self.player_status_attributes(status_header)
  status_header.split.map do |attribute|
    case attribute
      when 'connected'
        :time
      when 'frag'
        :score
      else
        attribute.to_sym
    end
  end
end

.split_player_status(attributes, player_status) ⇒ Hash<Symbol, String>

Splits the player status obtained with ‘rcon status`

Parameters:

  • attributes (Array<Symbol>)

    The attribute names

  • player_status (String)

    The status line of a single player

Returns:

  • (Hash<Symbol, String>)

    The attributes with the corresponding values for this player

See Also:



53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
# File 'lib/steam/servers/game_server.rb', line 53

def self.split_player_status(attributes, player_status)
  player_status.sub! /^\d+ +/, '' if attributes.first != :userid

  first_quote = player_status.index '"'
  last_quote  = player_status.rindex '"'
  data = [
    player_status[0, first_quote],
    player_status[first_quote + 1..last_quote - 1],
    player_status[last_quote + 1..-1]
  ]
  data = [ data[0].split, data[1], data[2].split ]
  data.flatten!

  if attributes.size > data.size && attributes.include?(:state)
    data.insert 3, nil, nil, nil
  elsif attributes.size < data.size
    data.delete_at 1
  end

  player_data = {}
  data.each_index do |i|
    player_data[attributes[i]] = data[i]
  end

  player_data
end

Instance Method Details

#handle_response_for_request(request_type, repeat_on_failure = true) ⇒ Object

Sends the specified request to the server and handles the returned response

Depending on the given request type this will fill the various data attributes of the server object.

Parameters:

  • request_type (Symbol)

    The type of request to send to the server

  • repeat_on_failure (Boolean) (defaults to: true)

    Whether the request should be repeated, if the replied packet isn’t expected. This is useful to handle missing challenge numbers, which will be automatically filled in, although not requested explicitly.

Raises:



203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
# File 'lib/steam/servers/game_server.rb', line 203

def handle_response_for_request(request_type, repeat_on_failure = true)
  case request_type
    when :challenge then
      request_packet = A2S_PLAYER_Packet.new
      expected_response = S2C_CHALLENGE_Packet
    when :info then
      request_packet = A2S_INFO_Packet.new
      expected_response = S2A_INFO_BasePacket
    when :players then
      request_packet = A2S_PLAYER_Packet.new(@challenge_number)
      expected_response = S2A_PLAYER_Packet
    when :rules then
      request_packet = A2S_RULES_Packet.new(@challenge_number)
      expected_response = S2A_RULES_Packet
    else
      raise SteamCondenserError, 'Called with wrong request type.'
  end

  send_request request_packet
  response_packet = reply

  if response_packet.kind_of? S2A_INFO_BasePacket
    @info_hash = response_packet.info
  elsif response_packet.kind_of? S2A_PLAYER_Packet
    @player_hash = response_packet.player_hash
  elsif response_packet.kind_of? S2A_RULES_Packet
    @rules_hash = response_packet.rules_hash
  elsif response_packet.kind_of? S2C_CHALLENGE_Packet
    @challenge_number = response_packet.challenge_number
  else
    raise SteamCondenserError, "Response of type #{response_packet.class} cannot be handled by this method."
  end

  unless response_packet.kind_of? expected_response
    puts "Expected #{expected_response}, got #{response_packet.class}." if $DEBUG
    handle_response_for_request(request_type, false) if repeat_on_failure
  end
end

#initObject

Initializes this server object with basic information



247
248
249
250
251
# File 'lib/steam/servers/game_server.rb', line 247

def init
  update_ping
  update_server_info
  update_challenge_number
end

#initialize(address, port = 27015) ⇒ Object

Creates a new instance of a game server object

Parameters:

  • address (String)

    Either an IP address, a DNS name or one of them combined with the port number. If a port number is given, e.g. ‘server.example.com:27016’ it will override the second argument.

  • port (Fixnum) (defaults to: 27015)

    The port the server is listening on

Raises:



87
88
89
90
91
# File 'lib/steam/servers/game_server.rb', line 87

def initialize(address, port = 27015)
  super

  @rcon_authenticated = false
end

#pingFixnum

Returns the last measured response time of this server

If the latency hasn’t been measured yet, it is done when calling this method for the first time.

If this information is vital to you, be sure to call #update_ping regularly to stay up-to-date.

Returns:

  • (Fixnum)

    The latency of this server in milliseconds

See Also:



103
104
105
106
# File 'lib/steam/servers/game_server.rb', line 103

def ping
  update_ping if @ping.nil?
  @ping
end

#players(rcon_password = nil) ⇒ Hash

Returns a list of players currently playing on this server

If the players haven’t been fetched yet, it is done when calling this method for the first time.

As the players and their scores change quite often be sure to update this list regularly by calling #update_players if you rely on this information.

Parameters:

  • rcon_password (String) (defaults to: nil)

    The RCON password of this server may be provided to gather more detailed information on the players, like STEAM_IDs.

Returns:

  • (Hash)

    The players on this server

See Also:



122
123
124
125
# File 'lib/steam/servers/game_server.rb', line 122

def players(rcon_password = nil)
  update_players(rcon_password) if @player_hash.nil?
  @player_hash
end

#rcon_auth(password) ⇒ Boolean

This method is abstract.

Must be be implemented by including classes to handle the authentication

Authenticates the connection for RCON communication with the server

Parameters:

  • password (String)

    The RCON password of the server

Returns:

  • (Boolean)

    whether the authentication was successful

Raises:

  • (NotImplementedError)

See Also:



134
135
136
# File 'lib/steam/servers/game_server.rb', line 134

def rcon_auth(password)
  raise NotImplementedError
end

#rcon_authenticated?Boolean

Returns whether the RCON connection to this server is already authenticated

Returns:

  • (Boolean)

    ‘true` if the RCON connection is authenticated

See Also:



142
143
144
# File 'lib/steam/servers/game_server.rb', line 142

def rcon_authenticated?
  @rcon_authenticated
end

#rcon_exec(command) ⇒ String

This method is abstract.

Must be be implemented by including classes to handle the command execution

Remotely executes a command on the server via RCON

Parameters:

  • command (String)

    The command to execute on the server

Returns:

  • (String)

    The output of the executed command

Raises:

  • (NotImplementedError)

See Also:



153
154
155
# File 'lib/steam/servers/game_server.rb', line 153

def rcon_exec(command)
  raise NotImplementedError
end

#rulesHash<String, String>

Returns the settings applied on the server. These settings are also called rules.

If the rules haven’t been fetched yet, it is done when calling this method for the first time.

As the rules usually don’t change often, there’s almost no need to update this hash. But if you need to, you can achieve this by calling #update_rules.

Returns:

  • (Hash<String, String>)

    The currently active server rules

See Also:



169
170
171
172
# File 'lib/steam/servers/game_server.rb', line 169

def rules
  update_rules if @rules_hash.nil?
  @rules_hash
end

#server_infoHash

Returns a hash with basic information on the server.

If the server information haven’t been fetched yet, it is done when calling this method for the first time.

The server information usually only changes on map change and when players join or leave. As the latter changes can be monitored by calling #update_players, there’s no need to call #update_server_info very often.

Returns:

  • (Hash)

    Server attributes with their values

See Also:



186
187
188
189
# File 'lib/steam/servers/game_server.rb', line 186

def server_info
  update_server_info if @info_hash.nil?
  @info_hash
end

#to_sString

Returns a human-readable text representation of the server

Returns:

  • (String)

    Available information about the server in a human-readable format



347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
# File 'lib/steam/servers/game_server.rb', line 347

def to_s
  return_string = ''

  return_string << "Ping: #@ping\n"
  return_string << "Challenge number: #@challenge_number\n"

  unless @info_hash.nil?
    return_string << "Info:\n"
    @info_hash.each do |key, value|
      return_string << "  #{key}: #{value.inspect}\n"
    end
  end

  unless @player_hash.nil?
    return_string << "Players:\n"
    @player_hash.each_value do |player|
      return_string << "  #{player}\n"
    end
  end

  unless @rules_hash.nil?
    return_string << "Rules:\n"
    @rules_hash.each do |key, value|
      return_string << "  #{key}: #{value}\n"
    end
  end

  return_string
end

#update_challenge_numberObject

Sends a A2S_SERVERQUERY_GETCHALLENGE request to the server and updates the challenge number used to communicate with this server

There’s usually no need to call this method explicitly, because #handle_response_for_request will automatically get the challenge number when the server assigns a new one.



323
324
325
# File 'lib/steam/servers/game_server.rb', line 323

def update_challenge_number
  handle_response_for_request :challenge
end

#update_pingFixnum

Sends a A2S_INFO request to the server and measures the time needed for the reply

If this information is vital to you, be sure to call this method regularly to stay up-to-date.

Returns:

  • (Fixnum)

    The latency of this server in milliseconds

See Also:



335
336
337
338
339
340
341
# File 'lib/steam/servers/game_server.rb', line 335

def update_ping
  send_request A2S_INFO_Packet.new
  start_time = Time.now
  reply
  end_time = Time.now
  @ping = (end_time - start_time) * 1000
end

#update_players(rcon_password = nil) ⇒ Object

Sends a A2S_PLAYERS request to the server and updates the players’ data for this server

As the players and their scores change quite often be sure to update this list regularly by calling this method if you rely on this information.

Parameters:

  • rcon_password (String) (defaults to: nil)

    The RCON password of this server may be provided to gather more detailed information on the players, like STEAM_IDs.

See Also:



265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
# File 'lib/steam/servers/game_server.rb', line 265

def update_players(rcon_password = nil)
  handle_response_for_request :players

  unless @rcon_authenticated
    return if rcon_password.nil?
    rcon_auth rcon_password
  end

  players = rcon_exec('status').lines.select do |line|
    line.start_with?('#') && line != "#end\n"
  end.map do |line|
    line[1..-1].strip
  end
  attributes = GameServer.player_status_attributes players.shift

  players.each do |player|
    player_data = GameServer.split_player_status(attributes, player)
    player_name = player_data[:name]
    if @player_hash.key? player_name
      @player_hash[player_name].add_info player_data
    end
  end
end

#update_rulesObject

Sends a A2S_RULES request to the server and updates the rules of this server

As the rules usually don’t change often, there’s almost no need to update this hash. But if you need to, you can achieve this by calling this method.



297
298
299
# File 'lib/steam/servers/game_server.rb', line 297

def update_rules
  handle_response_for_request :rules
end

#update_server_infoObject

Sends a A2S_INFO request to the server and updates this server’s basic information

The server information usually only changes on map change and when players join or leave. As the latter changes can be monitored by calling #update_players, there’s no need to call this method very often.



310
311
312
# File 'lib/steam/servers/game_server.rb', line 310

def update_server_info
  handle_response_for_request :info
end