Class: GamespyQuery::Socket

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

Constant Summary collapse

DEFAULT_TIMEOUT =
3
MAX_PACKETS =
7
ID_PACKET =

TODO: Randomize?

[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 =
1500
STR_HOSTNAME =
"hostname"
STR_PLAYERS =
"players"
STR_DEATHS =
"deaths_\x00\x00"
STR_PLAYER =
"player_\x00\x00"
STR_TEAM =
"team_\x00\x00"
STR_SCORE =
"score_\x00\x00"
SPLIT =
STR_X0
STR_END =
"\x00\x02"
STR_EMPTY =
Tools::STR_EMPTY
STR_BLA =
"%c%c%c%c".encode("ASCII-8BIT")
STR_GARBAGE =
"\x00\x04\x05\x06\a"
RX_PLAYER_EMPTY =
/^player_\x00\x00\x00/
RX_PLAYER_INFO =

x00 from previous packet, x01 from continueing player info, (.) - should it overwrite previous value?

/\x01(team|player|score|deaths)_.(.)/
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, #clean_string, #get_string, #handle_chr, #strip_tags

Constructor Details

#initialize(addr, address_family = ::Socket::AF_INET) ⇒ Socket

Returns a new instance of Socket.



120
121
122
123
124
125
126
127
# File 'lib/gamespy_query/socket.rb', line 120

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

#addrObject

Returns the value of attribute addr.



118
119
120
# File 'lib/gamespy_query/socket.rb', line 118

def addr
  @addr
end

#dataObject

Returns the value of attribute data.



118
119
120
# File 'lib/gamespy_query/socket.rb', line 118

def data
  @data
end

#failedObject

Returns the value of attribute failed.



118
119
120
# File 'lib/gamespy_query/socket.rb', line 118

def failed
  @failed
end

#max_packetsObject

Returns the value of attribute max_packets.



118
119
120
# File 'lib/gamespy_query/socket.rb', line 118

def max_packets
  @max_packets
end

#needs_challengeObject

Returns the value of attribute needs_challenge.



118
119
120
# File 'lib/gamespy_query/socket.rb', line 118

def needs_challenge
  @needs_challenge
end

#stampObject

Returns the value of attribute stamp.



118
119
120
# File 'lib/gamespy_query/socket.rb', line 118

def stamp
  @stamp
end

#stateObject

Returns the value of attribute state.



118
119
120
# File 'lib/gamespy_query/socket.rb', line 118

def state
  @state
end

Instance Method Details

#fetchObject



269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
# File 'lib/gamespy_query/socket.rb', line 269

def fetch
  pings = []
  r = self.data
  begin
    until valid?
      if handle_state
        if IO.select(nil, [self], nil, DEFAULT_TIMEOUT)
          handle_write
        else
          raise "TimeOut"
        end
      else
        if IO.select([self], nil, nil, DEFAULT_TIMEOUT)
          handle_read
        else
          raise "TimeOut"
        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
    Tools.log_exception(e)
    r = nil
    close unless closed?
  end
  r
end

#handle_challenge(str) ⇒ Object



241
242
243
244
245
246
247
248
249
# File 'lib/gamespy_query/socket.rb', line 241

def handle_challenge str
  # Tools.debug{"Received challenge response (#{str.length}): #{str.inspect}"}
  need_challenge = !(str.sub(STR_X0, STR_EMPTY) =~ RX_NO_CHALLENGE)
  if need_challenge
    str = str.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

#handle_excObject



216
217
218
219
220
221
222
# File 'lib/gamespy_query/socket.rb', line 216

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

  false
end

#handle_readObject



168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
# File 'lib/gamespy_query/socket.rb', line 168

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

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

        handle_challenge get_string(data[0])

        self.state = STATE_RECEIVED_CHALLENGE
      rescue => e
        Tools.log_exception e
        self.failed = true
        r = false
        close unless closed?
      end
    when STATE_SENT_CHALLENGE_RESPONSE, STATE_RECEIVE_DATA
      begin
        data = self.recvfrom_nonblock(RECEIVE_SIZE)
        Tools.debug {"Read (3,4): #{self.inspect}: #{data}"}
        self.state = STATE_RECEIVE_DATA

        game_data = get_string(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
      rescue => e
        Tools.log_exception(e)
        self.failed = true
        r = false
        close unless closed?
      end
  end
  r
end

#handle_splitnum(game_data) ⇒ Object



224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
# File 'lib/gamespy_query/socket.rb', line 224

def handle_splitnum game_data
  index = 0
  if game_data.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

#handle_stateObject



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

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

#handle_writeObject



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
166
# File 'lib/gamespy_query/socket.rb', line 133

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
    end
  rescue => e
    Tools.log_exception e
    self.failed = true
    r = false
    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

#sync(reply = self.fetch) ⇒ Object

Supports challenge/response and multi-packet



254
255
256
257
258
259
260
261
262
263
264
265
266
267
# File 'lib/gamespy_query/socket.rb', line 254

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

#valid?Boolean

Returns:

  • (Boolean)


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

def valid?; @state == STATE_READY; end