Class: GamespyQuery::Socket

Inherits:
UDPSocket
  • Object
show all
Includes:
Funcs
Defined in:
lib/gamespy_query/socket.rb

Overview

Provides direct connection functionality to gamespy enabled game servers This query contains up to 7x more information than the gamespy master browser query For example, player lists with info (teams, scores, deaths) are only available by using direct connection

Defined Under Namespace

Classes: NotInReadState, NotInWriteState

Constant Summary

DEFAULT_TIMEOUT =

Default timeout per connection state

3
MAX_PACKETS =

Maximum amount of packets sent by the server This is a limit set by gamespy

7
ID_PACKET =

Packet bits

[0x04, 0x05, 0x06, 0x07].pack("c*")
BASE_PACKET =
[0xFE, 0xFD, 0x00].pack("c*")
CHALLENGE_PACKET =
[0xFE, 0xFD, 0x09].pack("c*")
FULL_INFO_PACKET_MP =
[0xFF, 0xFF, 0xFF, 0x01].pack("c*")
FULL_INFO_PACKET =
[0xFF, 0xFF, 0xFF].pack("c*")
SERVER_INFO_PACKET =
[0xFF, 0x00, 0x00].pack("c*")
PLAYER_INFO_PACKET =
[0x00, 0xFF, 0x00].pack("c*")
RECEIVE_SIZE =

Maximum receive size

1500
STR_EMPTY =
Tools::STR_EMPTY
STR_BLA =
"%c%c%c%c".encode("ASCII-8BIT")
STR_GARBAGE =
"\x00\x04\x05\x06\a"
RX_NO_CHALLENGE =
/0@0$/
RX_CHALLENGE =
/0@/
RX_CHALLENGE2 =
/[^0-9\-]/si
RX_SPLITNUM =
/^splitnum\x00(.)/i

Constants included from Funcs

Funcs::RX_F, Funcs::RX_I, Funcs::RX_S

Instance Attribute Summary (collapse)

Instance Method Summary (collapse)

Methods included from Funcs

#clean, #encode_string, #handle_chr, #strip_tags

Constructor Details

- (Socket) initialize(addr, address_family = ::Socket::AF_INET)

Initializes the object

Parameters:

  • addr (String)

    Server address (“ip:port”)

  • address_family (Address Family) (defaults to: ::Socket::AF_INET)


52
53
54
55
56
57
58
59
# File 'lib/gamespy_query/socket.rb', line 52

def initialize(addr, address_family = ::Socket::AF_INET)
  @addr, @data, @state, @max_packets = addr, {}, 0, MAX_PACKETS
  @id_packet = ID_PACKET
  @packet = CHALLENGE_PACKET + @id_packet

  super(address_family)
  self.connect(*addr.split(":"))
end

Instance Attribute Details

- (Object) addr

Returns the value of attribute addr



47
48
49
# File 'lib/gamespy_query/socket.rb', line 47

def addr
  @addr
end

- (Object) data

Returns the value of attribute data



47
48
49
# File 'lib/gamespy_query/socket.rb', line 47

def data
  @data
end

- (Object) failed

Returns the value of attribute failed



47
48
49
# File 'lib/gamespy_query/socket.rb', line 47

def failed
  @failed
end

- (Object) max_packets

Returns the value of attribute max_packets



47
48
49
# File 'lib/gamespy_query/socket.rb', line 47

def max_packets
  @max_packets
end

- (Object) needs_challenge

Returns the value of attribute needs_challenge



47
48
49
# File 'lib/gamespy_query/socket.rb', line 47

def needs_challenge
  @needs_challenge
end

- (Object) stamp

Returns the value of attribute stamp



47
48
49
# File 'lib/gamespy_query/socket.rb', line 47

def stamp
  @stamp
end

- (Object) state

Returns the value of attribute state



47
48
49
# File 'lib/gamespy_query/socket.rb', line 47

def state
  @state
end

Instance Method Details

- (Object) fetch

Fetch all packets from socket



230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
# File 'lib/gamespy_query/socket.rb', line 230

def fetch
  pings = []
  r = self.data
  begin
    until valid?
      if handle_state
        if IO.select(nil, [self], nil, DEFAULT_TIMEOUT)
          handle_write
        else
          raise TimeOutError, "TimeOut during write, #{self}"
        end
      else
        if IO.select([self], nil, nil, DEFAULT_TIMEOUT)
          handle_read
        else
          raise TimeOutError, "TimeOut during read, #{self}"
        end
      end
    end
    data.each_pair {|k, d| Tools.debug {"GSPY Infos: #{k} #{d.size}"} } unless @silent || !$debug

    pings.map!{|ping| (ping * 1000).round}
    pings_c = 0
    pings.each { |ping| pings_c += ping }

    ping = pings.size == 0 ? nil : pings_c / pings.size
    Tools.debug{"Gamespy pings: #{pings}, #{ping}"}
    @ping = ping
  rescue => e
    # TODO: Simply raise the exception?
    Tools.log_exception(e)
    r = nil
    close unless closed?
  end
  r
end

- (Object) handle_challenge(packet)

Handle the challenge/response, if the server requires it

Parameters:

  • packet (String)

    Packet to process for challenge/response



198
199
200
201
202
203
204
205
206
# File 'lib/gamespy_query/socket.rb', line 198

def handle_challenge packet
  # Tools.debug{"Received challenge response (#{packet.length}): #{packet.inspect}"}
  need_challenge = !(packet.sub(STR_X0, STR_EMPTY) =~ RX_NO_CHALLENGE)
  if need_challenge
    str = packet.sub(RX_CHALLENGE, STR_EMPTY).gsub(RX_CHALLENGE2, STR_EMPTY).to_i
    challenge_packet = sprintf(STR_BLA, handle_chr(str >> 24), handle_chr(str >> 16), handle_chr(str >> 8), handle_chr(str >> 0))
    self.needs_challenge = challenge_packet
  end
end

- (Object) handle_exc

Handle the exception state TODO



169
170
171
172
173
174
175
# File 'lib/gamespy_query/socket.rb', line 169

def handle_exc
  Tools.debug {"Exception: #{self.inspect}"}
  close unless closed?
  self.failed = true

  false
end

- (Object) handle_read

Handle the read state



119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
# File 'lib/gamespy_query/socket.rb', line 119

def handle_read
  # Tools.debug {"Read: #{self.inspect}, #{self.state}"}

  r = true
  begin
    case self.state
      when STATE_SENT_CHALLENGE
        data = self.recvfrom_nonblock(RECEIVE_SIZE)
        Tools.debug {"Read (1): #{self.inspect}: #{data}"}

        handle_challenge data[0]

        self.state = STATE_RECEIVED_CHALLENGE
      when STATE_SENT_CHALLENGE_RESPONSE, STATE_RECEIVE_DATA
        data = self.recvfrom_nonblock(RECEIVE_SIZE)
        Tools.debug {"Read (3,4): #{self.inspect}: #{data}"}
        self.state = STATE_RECEIVE_DATA

        game_data = data[0]
        Tools.debug {"Received (#{self.data.size + 1}):\n\n#{game_data.inspect}\n\n#{game_data}\n\n"}

        index = handle_splitnum game_data

        self.data[index] = game_data

        if self.data.size >= self.max_packets # OR we received the end-packet and all packets required
          Tools.debug {"Received packet limit: #{self.inspect}"}
          self.state = STATE_READY
          r = false
          close unless closed?
        end
      else
        raise NotInReadState, "NotInReadState, #{self}"
    end
  rescue NotInReadState => e
    r = false
    self.failed = true
    close unless closed?
  rescue => e
    # TODO: Simply raise the exception?
    Tools.log_exception(e)
    self.failed = true
    r = nil
    close unless closed?
  end
  r
end

- (Object) handle_splitnum(packet)

Process the splitnum provided in the packet

Parameters:

  • packet (String)

    Packet data



179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
# File 'lib/gamespy_query/socket.rb', line 179

def handle_splitnum packet
  index = 0
  if packet.sub(STR_GARBAGE, STR_EMPTY)[RX_SPLITNUM]
    splitnum = $1
    flag = splitnum.unpack("C")[0]
    index = (flag & 127).to_i
    last = flag & 0x80 > 0
    # Data could be received out of order, use the "index" id when "last" flag is true, to determine total packet_count
    self.max_packets = index + 1 if last # update the max
    Tools.debug {"Splitnum: #{splitnum.inspect} (#{splitnum}) (#{flag}, #{index}, #{last}) Max: #{self.max_packets}"}
  else
    self.max_packets = 1
  end

  index
end

- (Object) handle_state

Determine Read/Write/Exception state



209
# File 'lib/gamespy_query/socket.rb', line 209

def handle_state; [STATE_INIT, STATE_RECEIVED_CHALLENGE].include? state; end

- (Object) handle_write

Handle the write state



77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
# File 'lib/gamespy_query/socket.rb', line 77

def handle_write
  #Tools.debug {"Write: #{self.inspect}, #{self.state}"}

  r = true
  begin
    case self.state
      when STATE_INIT
        Tools.debug {"Write (0): #{self.inspect}"}
        # Send Challenge request
        self.puts @packet
        self.state = STATE_SENT_CHALLENGE
      when STATE_RECEIVED_CHALLENGE
        Tools.debug {"Write (2): #{self.inspect}"}
        # Send Challenge response
        self.puts self.needs_challenge ? BASE_PACKET + @id_packet + self.needs_challenge + FULL_INFO_PACKET_MP : BASE_PACKET + @id_packet + FULL_INFO_PACKET_MP
        self.state = STATE_SENT_CHALLENGE_RESPONSE
      else
        raise NotInWriteState, "NotInWriteState, #{self}"
    end
  rescue NotInWriteState => e
    r = false
    self.failed = true
    close unless closed?
  rescue => e
    Tools.log_exception e
    self.failed = true
    r = nil
    close unless closed?
  end

=begin
if Time.now - self.stamp > @timeout
  Tools.debug {"TimedOut: #{self.inspect}"}
  self.failed = true
  r = false
  close unless closed?
end
=end
  r
end

- (Object) sync(reply = self.fetch)

Process data Supports challenge/response and multi-packet

Parameters:

  • reply (String) (defaults to: self.fetch)

    Reply from server



214
215
216
217
218
219
220
221
222
223
224
225
226
227
# File 'lib/gamespy_query/socket.rb', line 214

def sync reply = self.fetch
  game_data, key = {}, nil
  return game_data if reply.nil? || reply.empty?

  parser = Parser.new(reply)
  data = parser.parse

  game_data.merge!(data[:game])
  game_data[:players] = Parser.pretty_player_data2(data[:players]).sort {|a, b| a[:name].downcase <=> b[:name].downcase }

  game_data[:ping] = @ping unless @ping.nil?

  game_data
end

- (Boolean) valid?

Is the socket state valid? Only if all states have passed

Returns:

  • (Boolean)


74
# File 'lib/gamespy_query/socket.rb', line 74

def valid?; @state == STATE_READY; end