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

Constant Summary collapse

REQUEST_CHALLENGE =
0
REQUEST_INFO =
1
REQUEST_PLAYER =
2
REQUEST_RULES =
3

Instance Attribute Summary

Attributes included from Server

#host_names, #ip_addresses

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Server

#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:



37
38
39
40
41
42
43
44
45
46
47
48
# File 'lib/steam/servers/game_server.rb', line 37

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:



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

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 (Fixnum)

    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:



199
200
201
202
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 199

def handle_response_for_request(request_type, repeat_on_failure = true)
  begin
    case request_type
      when GameServer::REQUEST_CHALLENGE then
        request_packet = A2S_SERVERQUERY_GETCHALLENGE_Packet.new
        expected_response = S2C_CHALLENGE_Packet
      when GameServer::REQUEST_INFO then
        request_packet = A2S_INFO_Packet.new
        expected_response = S2A_INFO_BasePacket
      when GameServer::REQUEST_PLAYER then
        request_packet = A2S_PLAYER_Packet.new(@challenge_number)
        expected_response = S2A_PLAYER_Packet
      when GameServer::REQUEST_RULES then
        request_packet = A2S_RULES_Packet.new(@challenge_number)
        expected_response = S2A_RULES_Packet
      else
        raise SteamCondenserException.new("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_hash
    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 SteamCondenserException.new("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
  rescue TimeoutException
    puts "Expected #{expected_response}, but timed out." if $DEBUG
  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:



91
92
93
# File 'lib/steam/servers/game_server.rb', line 91

def initialize(address, port = 27015)
  super
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:



105
106
107
108
# File 'lib/steam/servers/game_server.rb', line 105

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:



124
125
126
127
# File 'lib/steam/servers/game_server.rb', line 124

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

See Also:



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

def rcon_auth(password)
end

#rcon_authenticated?Boolean

Returns:

  • (Boolean)


139
140
141
# File 'lib/steam/servers/game_server.rb', line 139

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

See Also:



150
151
# File 'lib/steam/servers/game_server.rb', line 150

def rcon_exec(command)
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:



165
166
167
168
# File 'lib/steam/servers/game_server.rb', line 165

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:



182
183
184
185
# File 'lib/steam/servers/game_server.rb', line 182

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



344
345
346
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
# File 'lib/steam/servers/game_server.rb', line 344

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.



320
321
322
# File 'lib/steam/servers/game_server.rb', line 320

def update_challenge_number
  handle_response_for_request GameServer::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:



332
333
334
335
336
337
338
# File 'lib/steam/servers/game_server.rb', line 332

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
# File 'lib/steam/servers/game_server.rb', line 265

def update_players(rcon_password = nil)
  handle_response_for_request GameServer::REQUEST_PLAYER

  unless rcon_password.nil? || @player_hash.nil? || @player_hash.empty?
    rcon_auth rcon_password
    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)
      if @player_hash.key? player_data[:name]
        @player_hash[player_data[:name]].add_info player_data
      end
    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.



294
295
296
# File 'lib/steam/servers/game_server.rb', line 294

def update_rules
  handle_response_for_request GameServer::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.



307
308
309
# File 'lib/steam/servers/game_server.rb', line 307

def update_server_info
  handle_response_for_request GameServer::REQUEST_INFO
end