gamespy_query 0.1.5 → 0.2.0pre

Sign up to get free protection for your applications and to get access to all the features.
@@ -7,9 +7,8 @@
7
7
  Gamedata values are not split, Player lists can be (names, teams, scores, deaths), while individual values still are not.
8
8
  =end
9
9
 
10
- require_relative 'base'
11
-
12
10
  module GamespyQuery
11
+ # Parsing gamespy query packets and processing them to Hash
13
12
  class Parser < Base
14
13
  STR_SPLIT = STR_X0
15
14
  STR_ID = "\x00\x04\x05\x06\a"
@@ -18,7 +17,11 @@ module GamespyQuery
18
17
  RX_PLAYER_HEADER = /\x01/
19
18
  RX_END = /\x00\x02$/
20
19
 
21
- # packets:
20
+ # Packets to process
21
+ attr_reader :packets
22
+
23
+ # Initializes the object
24
+ # @param [Hash or Array] packets
22
25
  # - Hash, key: packetID, value: packetDATA
23
26
  # or
24
27
  # - Array, packetDATA ordered already by packetID
@@ -33,20 +36,20 @@ module GamespyQuery
33
36
  end
34
37
  end
35
38
 
39
+ # Parse game and player data to hash
36
40
  # Returns Hash with parsed data (:game and :players)
37
- # :game => Hash, Key: InfoKey, Value: InfoValue
38
- # :players => Hash, Key: InfoType, Value: Array of Values
41
+ # game Key: InfoKey, Value: InfoValue
42
+ # players Key: InfoType, Value: Array of Values
39
43
  def parse
40
44
  data = {}
41
45
  data[:game] = {} # Key: InfoKey, Value: InfoValue
42
46
  data[:players] = {} # Key: InfoType, Value: Array of Values
43
47
  player_info = false
44
- player_data = ""
48
+ player_data = "".encode "ASCII-8BIT"
45
49
 
46
50
  # Parse the packets
47
51
  @packets.each do |packet|
48
- packet = clean_packet(packet)
49
-
52
+ packet = clean_packet packet
50
53
  if player_info
51
54
  # Player header was found before, add packet to player_data
52
55
  player_data += packet
@@ -74,6 +77,8 @@ module GamespyQuery
74
77
  data
75
78
  end
76
79
 
80
+ # Clean packet from useless data
81
+ # @param [String] packet Packet data
77
82
  def clean_packet(packet)
78
83
  packet = packet.clone
79
84
  packet.sub!(STR_ID, STR_EMPTY) # Cut off the identity
@@ -82,11 +87,12 @@ module GamespyQuery
82
87
  packet.sub!(RX_X0_S, STR_EMPTY) # Cut off first \x00
83
88
  packet.sub!(RX_END, STR_EMPTY) # Cut off the last \x00\x02
84
89
 
85
- # Encoding
86
- get_string(packet)
90
+ packet
87
91
  end
88
-
89
- def parse_game_data(packet)
92
+
93
+ # Parse game data in packet
94
+ # @param [String] packet Packet to parse
95
+ def parse_game_data(packet)
90
96
  Tools.debug {"Game Parsing #{packet.inspect}"}
91
97
 
92
98
  key = nil
@@ -94,9 +100,9 @@ module GamespyQuery
94
100
 
95
101
  packet.split(STR_SPLIT).each_with_index do |data, index|
96
102
  if (index % 2) == 0
97
- key = clean_string data
103
+ key = encode_string data
98
104
  else
99
- game_data[key] = data.is_a?(String) ? clean_string(data) : data
105
+ game_data[key] = data.is_a?(String) ? encode_string(data) : data
100
106
  end
101
107
  end
102
108
 
@@ -112,11 +118,13 @@ module GamespyQuery
112
118
  STR_SIX = "$SIX_OVERWRITE_PREVIOUS$"
113
119
  STR_SIX_X0 = "\x00#{STR_SIX}\x00"
114
120
 
121
+ # Parse player data in packet
122
+ # @param [String] packet Packet to parse
115
123
  # TODO: Cleanup
116
124
  def parse_player_data(packet)
117
125
  Tools.debug {"Player Parsing #{packet.inspect}"}
118
126
 
119
- player_data = {:names => [], :teams => [], :scores => [], :deaths => []} # [[], [], [], []]
127
+ player_data = {names: [], teams: [], scores: [], deaths: []} # [[], [], [], []]
120
128
 
121
129
  return player_data if packet.nil? || packet.empty?
122
130
 
@@ -154,7 +162,7 @@ module GamespyQuery
154
162
  # Parse the data - \x00 is printed after a non-nil entry, otherwise \x00 means nil (e.g empty team)
155
163
  until str.empty?
156
164
  entry = str[RX_X0_SPEC]
157
- player_data[player_data.keys[i]] << clean_string(entry.sub(STR_X0, STR_EMPTY))
165
+ player_data[player_data.keys[i]] << encode_string(entry.sub(STR_X0, STR_EMPTY))
158
166
  str.sub!(entry, STR_EMPTY)
159
167
  end
160
168
 
@@ -166,6 +174,7 @@ module GamespyQuery
166
174
  overwrite = true # tag so that the next entry will overwrite the latest entry
167
175
  next # ignore
168
176
  else
177
+ info = clean(info) if [2,3].include?(i) # Apply data_type conversion for Score and Deaths
169
178
  if overwrite
170
179
  new_player_data[-1] = info # Overwrite latest entry
171
180
  overwrite = false # done the overwrite
@@ -182,7 +191,8 @@ module GamespyQuery
182
191
  player_data
183
192
  end
184
193
 
185
- # Hash of Hashes
194
+ # Convert player data to Hash of Hashes
195
+ # @param [Hash] data Original player data split over 4 arrays in hash (:names, :teams, :scores, :deaths)
186
196
  def self.pretty_player_data(data)
187
197
  player_data = {}
188
198
 
@@ -196,7 +206,8 @@ module GamespyQuery
196
206
  player_data
197
207
  end
198
208
 
199
- # Array of Hashes
209
+ # Convert player data to Array of Hashes
210
+ # @param [Hash] data Original player data split over 4 arrays in hash (:names, :teams, :scores, :deaths)
200
211
  def self.pretty_player_data2(data)
201
212
  player_data = []
202
213
 
@@ -2,28 +2,31 @@
2
2
  # GameSpy query class by Sickboy [Patrick Roza] (sb_at_dev-heaven.net)
3
3
 
4
4
  require 'yaml'
5
- require_relative 'base'
6
- require_relative 'parser'
7
5
  require 'socket'
8
6
 
9
7
  module GamespyQuery
8
+ # Provides socket functionality on multiple platforms
10
9
  # TODO
11
10
  module MultiSocket
11
+ # Create socket
12
12
  def create_socket(*params)
13
13
  Tools.debug {"Creating socket #{params}"}
14
14
  _create_socket(*params)
15
15
  end
16
16
 
17
+ # Write socket
17
18
  def socket_send(*params)
18
19
  Tools.debug {"Sending socket #{params}"}
19
20
  _socket_send(*params)
20
21
  end
21
22
 
23
+ # Read socket
22
24
  def socket_receive(*params)
23
25
  Tools.debug {"Receiving socket #{params}"}
24
26
  _socket_receive(*params)
25
27
  end
26
28
 
29
+ # Close socket
27
30
  def socket_close(*params)
28
31
  Tools.debug {"Closing socket #{params}"}
29
32
  @s.close
@@ -33,6 +36,7 @@ module GamespyQuery
33
36
  include System::Net
34
37
  include System::Net::Sockets
35
38
 
39
+ # Create socket
36
40
  def _create_socket(host, port)
37
41
  @ip_end_point = IPEndPoint.new(IPAddress.Any, 0)
38
42
  @s = UdpClient.new
@@ -40,25 +44,30 @@ module GamespyQuery
40
44
  @s.connect(host, port.to_i)
41
45
  end
42
46
 
47
+ # Write socket
43
48
  def _socket_send(packet)
44
49
  @s.Send(packet, packet.length)
45
50
  end
46
51
 
52
+ # Read socket
47
53
  def _socket_receive
48
54
  @s.Receive(@ip_end_point)
49
55
  end
50
56
 
51
57
  else
52
58
 
59
+ # Create socket
53
60
  def _create_socket(host, port)
54
61
  @s = UDPSocket.new
55
62
  @s.connect(host, port)
56
63
  end
57
64
 
65
+ # Write socket
58
66
  def _socket_send(packet)
59
67
  @s.puts(packet)
60
68
  end
61
69
 
70
+ # Read socket
62
71
  def _socket_receive
63
72
  begin
64
73
  Timeout::timeout(DEFAULT_TIMEOUT) do
@@ -73,12 +82,20 @@ module GamespyQuery
73
82
  end
74
83
  end
75
84
 
85
+ # Provides direct connection functionality to gamespy enabled game servers
86
+ # This query contains up to 7x more information than the gamespy master browser query
87
+ # For example, player lists with info (teams, scores, deaths) are only available by using direct connection
76
88
  class Socket < UDPSocket
77
89
  include Funcs
78
90
 
91
+ # Default timeout per connection state
79
92
  DEFAULT_TIMEOUT = 3
93
+
94
+ # Maximum amount of packets sent by the server
95
+ # This is a limit set by gamespy
80
96
  MAX_PACKETS = 7
81
97
 
98
+ # Packet bits
82
99
  ID_PACKET = [0x04, 0x05, 0x06, 0x07].pack("c*") # TODO: Randomize?
83
100
  BASE_PACKET = [0xFE, 0xFD, 0x00].pack("c*")
84
101
  CHALLENGE_PACKET = [0xFE, 0xFD, 0x09].pack("c*")
@@ -88,6 +105,7 @@ module GamespyQuery
88
105
  SERVER_INFO_PACKET = [0xFF, 0x00, 0x00].pack("c*")
89
106
  PLAYER_INFO_PACKET = [0x00, 0xFF, 0x00].pack("c*")
90
107
 
108
+ # Maximum receive size
91
109
  RECEIVE_SIZE = 1500
92
110
 
93
111
  STR_HOSTNAME = "hostname"
@@ -117,6 +135,9 @@ module GamespyQuery
117
135
 
118
136
  attr_accessor :addr, :data, :state, :stamp, :needs_challenge, :max_packets, :failed
119
137
 
138
+ # Initializes the object
139
+ # @param [String] addr Server address ("ip:port")
140
+ # @param [Address Family] address_family
120
141
  def initialize(addr, address_family = ::Socket::AF_INET)
121
142
  @addr, @data, @state, @max_packets = addr, {}, 0, MAX_PACKETS
122
143
  @id_packet = ID_PACKET
@@ -126,10 +147,22 @@ module GamespyQuery
126
147
  self.connect(*addr.split(":"))
127
148
  end
128
149
 
150
+ # Exception
151
+ class NotInWriteState < StandardError
152
+ end
153
+
154
+ # Exception
155
+ class NotInReadState < StandardError
156
+ end
157
+
158
+ # Sets the state of the socket
159
+ # @param [Integer] state State to set
129
160
  def state=(state); @stamp = Time.now; @state = state; end
130
161
 
162
+ # Is the socket state valid? Only if all states have passed
131
163
  def valid?; @state == STATE_READY; end
132
164
 
165
+ # Handle the write state
133
166
  def handle_write
134
167
  #Tools.debug {"Write: #{self.inspect}, #{self.state}"}
135
168
 
@@ -146,11 +179,17 @@ module GamespyQuery
146
179
  # Send Challenge response
147
180
  self.puts self.needs_challenge ? BASE_PACKET + @id_packet + self.needs_challenge + FULL_INFO_PACKET_MP : BASE_PACKET + @id_packet + FULL_INFO_PACKET_MP
148
181
  self.state = STATE_SENT_CHALLENGE_RESPONSE
182
+ else
183
+ raise NotInWriteState, "NotInWriteState"
149
184
  end
185
+ rescue NotInWriteState => e
186
+ r = false
187
+ self.failed = true
188
+ close unless closed?
150
189
  rescue => e
151
190
  Tools.log_exception e
152
191
  self.failed = true
153
- r = false
192
+ r = nil
154
193
  close unless closed?
155
194
  end
156
195
 
@@ -165,32 +204,26 @@ module GamespyQuery
165
204
  r
166
205
  end
167
206
 
207
+ # Handle the read state
168
208
  def handle_read
169
209
  # Tools.debug {"Read: #{self.inspect}, #{self.state}"}
170
210
 
171
211
  r = true
172
- case self.state
173
- when STATE_SENT_CHALLENGE
174
- begin
212
+ begin
213
+ case self.state
214
+ when STATE_SENT_CHALLENGE
175
215
  data = self.recvfrom_nonblock(RECEIVE_SIZE)
176
216
  Tools.debug {"Read (1): #{self.inspect}: #{data}"}
177
217
 
178
- handle_challenge get_string(data[0])
218
+ handle_challenge data[0]
179
219
 
180
220
  self.state = STATE_RECEIVED_CHALLENGE
181
- rescue => e
182
- Tools.log_exception e
183
- self.failed = true
184
- r = false
185
- close unless closed?
186
- end
187
- when STATE_SENT_CHALLENGE_RESPONSE, STATE_RECEIVE_DATA
188
- begin
221
+ when STATE_SENT_CHALLENGE_RESPONSE, STATE_RECEIVE_DATA
189
222
  data = self.recvfrom_nonblock(RECEIVE_SIZE)
190
223
  Tools.debug {"Read (3,4): #{self.inspect}: #{data}"}
191
224
  self.state = STATE_RECEIVE_DATA
192
225
 
193
- game_data = get_string(data[0])
226
+ game_data = data[0]
194
227
  Tools.debug {"Received (#{self.data.size + 1}):\n\n#{game_data.inspect}\n\n#{game_data}\n\n"}
195
228
 
196
229
  index = handle_splitnum game_data
@@ -203,16 +236,25 @@ module GamespyQuery
203
236
  r = false
204
237
  close unless closed?
205
238
  end
206
- rescue => e
207
- Tools.log_exception(e)
208
- self.failed = true
209
- r = false
210
- close unless closed?
211
- end
239
+ else
240
+ raise NotInReadState, "NotInReadState"
241
+ end
242
+ rescue NotInReadState => e
243
+ r = false
244
+ self.failed = true
245
+ close unless closed?
246
+ rescue => e
247
+ # TODO: Simply raise the exception?
248
+ Tools.log_exception(e)
249
+ self.failed = true
250
+ r = nil
251
+ close unless closed?
212
252
  end
213
253
  r
214
254
  end
215
255
 
256
+ # Handle the exception state
257
+ # TODO
216
258
  def handle_exc
217
259
  Tools.debug {"Exception: #{self.inspect}"}
218
260
  close unless closed?
@@ -221,9 +263,11 @@ module GamespyQuery
221
263
  false
222
264
  end
223
265
 
224
- def handle_splitnum game_data
266
+ # Process the splitnum provided in the packet
267
+ # @param [String] packet Packet data
268
+ def handle_splitnum packet
225
269
  index = 0
226
- if game_data.sub(STR_GARBAGE, STR_EMPTY)[RX_SPLITNUM]
270
+ if packet.sub(STR_GARBAGE, STR_EMPTY)[RX_SPLITNUM]
227
271
  splitnum = $1
228
272
  flag = splitnum.unpack("C")[0]
229
273
  index = (flag & 127).to_i
@@ -238,19 +282,24 @@ module GamespyQuery
238
282
  index
239
283
  end
240
284
 
241
- def handle_challenge str
242
- # Tools.debug{"Received challenge response (#{str.length}): #{str.inspect}"}
243
- need_challenge = !(str.sub(STR_X0, STR_EMPTY) =~ RX_NO_CHALLENGE)
285
+ # Handle the challenge/response, if the server requires it
286
+ # @param [String] packet Packet to process for challenge/response
287
+ def handle_challenge packet
288
+ # Tools.debug{"Received challenge response (#{packet.length}): #{packet.inspect}"}
289
+ need_challenge = !(packet.sub(STR_X0, STR_EMPTY) =~ RX_NO_CHALLENGE)
244
290
  if need_challenge
245
- str = str.sub(RX_CHALLENGE, STR_EMPTY).gsub(RX_CHALLENGE2, STR_EMPTY).to_i
291
+ str = packet.sub(RX_CHALLENGE, STR_EMPTY).gsub(RX_CHALLENGE2, STR_EMPTY).to_i
246
292
  challenge_packet = sprintf(STR_BLA, handle_chr(str >> 24), handle_chr(str >> 16), handle_chr(str >> 8), handle_chr(str >> 0))
247
293
  self.needs_challenge = challenge_packet
248
294
  end
249
295
  end
250
296
 
297
+ # Determine Read/Write/Exception state
251
298
  def handle_state; [STATE_INIT, STATE_RECEIVED_CHALLENGE].include? state; end
252
299
 
300
+ # Process data
253
301
  # Supports challenge/response and multi-packet
302
+ # @param [String] reply Reply from server
254
303
  def sync reply = self.fetch
255
304
  game_data, key = {}, nil
256
305
  return game_data if reply.nil? || reply.empty?
@@ -266,6 +315,7 @@ module GamespyQuery
266
315
  game_data
267
316
  end
268
317
 
318
+ # Fetch all packets from socket
269
319
  def fetch
270
320
  pings = []
271
321
  r = self.data
@@ -275,13 +325,13 @@ module GamespyQuery
275
325
  if IO.select(nil, [self], nil, DEFAULT_TIMEOUT)
276
326
  handle_write
277
327
  else
278
- raise "TimeOut"
328
+ raise TimeOutError, "TimeOut"
279
329
  end
280
330
  else
281
331
  if IO.select([self], nil, nil, DEFAULT_TIMEOUT)
282
332
  handle_read
283
333
  else
284
- raise "TimeOut"
334
+ raise TimeOutError, "TimeOut"
285
335
  end
286
336
  end
287
337
  end
@@ -295,6 +345,7 @@ module GamespyQuery
295
345
  Tools.debug{"Gamespy pings: #{pings}, #{ping}"}
296
346
  @ping = ping
297
347
  rescue => e
348
+ # TODO: Simply raise the exception?
298
349
  Tools.log_exception(e)
299
350
  r = nil
300
351
  close unless closed?
@@ -303,18 +354,3 @@ module GamespyQuery
303
354
  end
304
355
  end
305
356
  end
306
-
307
- if $0 == __FILE__
308
- host, port = if ARGV.size > 1
309
- ARGV
310
- else
311
- ARGV[0].split(":")
312
- end
313
- time_start = Time.now
314
- g = GamespyQuery::Socket.new("#{host}:#{port}")
315
- r = g.sync
316
- time_taken = Time.now - time_start
317
- puts "Took: #{time_taken}s"
318
- exit unless r
319
- puts r.to_yaml
320
- end
@@ -1,18 +1,28 @@
1
- require_relative 'socket'
2
-
3
1
  module GamespyQuery
2
+ # Provides mass processing of Gamespy UDP sockets, by using Socket/IO select
4
3
  class SocketMaster < Base
4
+ # Should the current queue be extended to the maximum amount of connections, or should the queue be emptied first,
5
+ # before adding more?
5
6
  FILL_UP_ON_SPACE = true
7
+
8
+ # Default maximum concurrent connections
6
9
  DEFAULT_MAX_CONNECTIONS = 128
7
10
 
8
- attr_accessor :timeout, :max_connections
11
+ # Configurable timeout in seconds
12
+ attr_accessor :timeout
13
+
14
+ # Configurable max concurrenct connections
15
+ attr_accessor :max_connections
9
16
 
17
+ # Initializes the object
18
+ # @param [Array] addrs List of addresses to process
10
19
  def initialize addrs
11
20
  @addrs = addrs
12
21
 
13
22
  @timeout, @max_connections = Socket::DEFAULT_TIMEOUT, DEFAULT_MAX_CONNECTIONS # Per select iteration
14
23
  end
15
24
 
25
+ # Process the list of addresses
16
26
  def process!
17
27
  sockets = []
18
28
 
@@ -56,6 +66,42 @@ module GamespyQuery
56
66
 
57
67
  return sockets
58
68
  end
69
+
70
+ class <<self
71
+ # Fetch the gamespy master browser list
72
+ # Connect to each individual server to receive player data etc
73
+ # @param [String] game Game to fetch info from
74
+ # @param [String] geo Geo location enabled?
75
+ # @param [Array] remote Hostname and path+filename strings if the list needs to be fetched from http server
76
+ def process_master(game = "arma2oapc", geo = nil, remote = nil)
77
+ master = GamespyQuery::Master.new(geo, game)
78
+ list = if remote
79
+ Net::HTTP.start(remote[0]) do |http|
80
+ resp = http.get(remote[1])
81
+ resp.body.split("\n")
82
+ end
83
+ else
84
+ master.get_server_list(nil, true, geo)
85
+ end
86
+
87
+ dat = master.process list
88
+
89
+ sm = GamespyQuery::SocketMaster.new(dat.keys)
90
+ sockets = sm.process!
91
+ sockets.select{|s| s.valid? }.each do |s|
92
+ begin
93
+ data = dat[s.addr]
94
+ data[:ip], data[:port] = s.addr.split(":")
95
+ data[:gamename] = game
96
+ data[:gamedata].merge!(s.sync(s.data))
97
+ rescue => e
98
+ Tools.log_exception e
99
+ end
100
+ end
101
+
102
+ dat.values
103
+ end
104
+ end
59
105
  end
60
106
  end
61
107