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.
- data/.gitignore +5 -0
- data/README.md +7 -4
- data/Rakefile +14 -0
- data/bin/gamespy_query +8 -0
- data/gamespy_query.gemspec +3 -3
- data/lib/gamespy_query.rb +20 -5
- data/lib/gamespy_query/base.rb +45 -24
- data/lib/gamespy_query/master.rb +40 -13
- data/lib/gamespy_query/options.rb +140 -0
- data/lib/gamespy_query/parser.rb +29 -18
- data/lib/gamespy_query/socket.rb +81 -45
- data/lib/gamespy_query/socket_master.rb +49 -3
- data/lib/gamespy_query/version.rb +2 -1
- data/test/teststrap.rb +4 -0
- data/test/units/base_test.rb +43 -0
- data/test/units/master_test.rb +17 -0
- data/test/units/options_test.rb +27 -0
- data/test/units/parser_test.rb +47 -0
- data/test/units/socket_master_test.rb +17 -0
- data/test/units/socket_test.rb +73 -0
- metadata +51 -7
data/lib/gamespy_query/parser.rb
CHANGED
@@ -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
|
-
#
|
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
|
-
#
|
38
|
-
#
|
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
|
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
|
-
|
86
|
-
get_string(packet)
|
90
|
+
packet
|
87
91
|
end
|
88
|
-
|
89
|
-
|
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 =
|
103
|
+
key = encode_string data
|
98
104
|
else
|
99
|
-
game_data[key] = data.is_a?(String) ?
|
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 = {:
|
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]] <<
|
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
|
|
data/lib/gamespy_query/socket.rb
CHANGED
@@ -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 =
|
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
|
-
|
173
|
-
|
174
|
-
|
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
|
218
|
+
handle_challenge data[0]
|
179
219
|
|
180
220
|
self.state = STATE_RECEIVED_CHALLENGE
|
181
|
-
|
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 =
|
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
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
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
|
-
|
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
|
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
|
-
|
242
|
-
|
243
|
-
|
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 =
|
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
|
-
|
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
|
|