gamespy_query 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +4 -0
- data/Gemfile +4 -0
- data/Rakefile +1 -0
- data/gamespy_query.gemspec +24 -0
- data/lib/gamespy_query/base.rb +43 -0
- data/lib/gamespy_query/master.rb +81 -0
- data/lib/gamespy_query/parser.rb +231 -0
- data/lib/gamespy_query/socket.rb +268 -0
- data/lib/gamespy_query/version.rb +3 -0
- data/lib/gamespy_query.rb +8 -0
- metadata +55 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "gamespy_query/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "gamespy_query"
|
7
|
+
s.version = GamespyQuery::VERSION
|
8
|
+
s.authors = ["Patrick Roza"]
|
9
|
+
s.email = ["sb@dev-heaven.net"]
|
10
|
+
s.homepage = "http://dev-heaven.net"
|
11
|
+
s.summary = %q{Ruby library for accessing Gamespy services}
|
12
|
+
s.description = %q{}
|
13
|
+
|
14
|
+
s.rubyforge_project = "gamespy_query"
|
15
|
+
|
16
|
+
s.files = `git ls-files`.split("\n")
|
17
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
18
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
19
|
+
s.require_paths = ["lib"]
|
20
|
+
|
21
|
+
# specify any dependencies here; for example:
|
22
|
+
# s.add_development_dependency "rspec"
|
23
|
+
# s.add_runtime_dependency "rest-client"
|
24
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
#require 'six/tools'
|
2
|
+
require 'action_controller'
|
3
|
+
require 'logger'
|
4
|
+
|
5
|
+
module GamespyQuery
|
6
|
+
module Tools
|
7
|
+
STR_EMPTY = ""
|
8
|
+
|
9
|
+
module_function
|
10
|
+
def logger
|
11
|
+
ActionController::Base.logger ||= Logger.new("logger.log")
|
12
|
+
end
|
13
|
+
|
14
|
+
def debug(&block)
|
15
|
+
logger.debug yield
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
class Base
|
20
|
+
def strip_tags(str)
|
21
|
+
# TODO: Strip tags!!
|
22
|
+
str
|
23
|
+
end
|
24
|
+
|
25
|
+
STR_X00 = "\x00"
|
26
|
+
RX_F = /\A\-?[0-9][0-9]*\.[0-9]*\Z/
|
27
|
+
RX_I = /\A\-?[0-9][0-9]*\Z/
|
28
|
+
RX_S = /\A\-?0[0-9]+.*\Z/
|
29
|
+
|
30
|
+
def clean(value) # TODO: Force String, Integer, Float etc?
|
31
|
+
case value
|
32
|
+
when STR_X00
|
33
|
+
nil
|
34
|
+
when RX_F
|
35
|
+
value =~ RX_S ? strip_tags(value) : value.to_f
|
36
|
+
when RX_I
|
37
|
+
value =~ RX_S ? strip_tags(value) : value.to_i
|
38
|
+
else
|
39
|
+
strip_tags(value)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
require_relative 'base'
|
2
|
+
|
3
|
+
module GamespyQuery
|
4
|
+
class Master < Base
|
5
|
+
PARAMS = [:hostname, :gamever, :gametype, :gamemode, :numplayers, :maxplayers, :password, :equalModRequired, :mission, :mapname,
|
6
|
+
:mod, :signatures, :verifysignatures, :gamestate, :dedicated, :platform, :sv_battleeye, :language, :difficulty]
|
7
|
+
|
8
|
+
# TODO: Gspy v3 multipacket
|
9
|
+
|
10
|
+
DELIMIT = case RUBY_PLATFORM
|
11
|
+
when /-mingw32$/, /-mswin32$/
|
12
|
+
"\\"
|
13
|
+
else
|
14
|
+
"\\\\"
|
15
|
+
end
|
16
|
+
|
17
|
+
def geoip_path
|
18
|
+
return "" unless defined?(Rails)
|
19
|
+
|
20
|
+
case RUBY_PLATFORM
|
21
|
+
when /-mingw32$/, /-mswin32$/
|
22
|
+
File.join(Rails.root, "config").gsub("/", "\\")
|
23
|
+
else
|
24
|
+
File.join(Rails.root, "config")
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def initialize(geo = nil, game = "arma2oapc")
|
29
|
+
@geo, @game = geo, game
|
30
|
+
end
|
31
|
+
|
32
|
+
def process
|
33
|
+
@list = Hash.new
|
34
|
+
self.to_hash(self.read)
|
35
|
+
end
|
36
|
+
|
37
|
+
def read
|
38
|
+
geo = @geo ? @geo : "-Q 11 "
|
39
|
+
unless File.exists?(File.join(geoip_path, "GeoIP.dat"))
|
40
|
+
puts
|
41
|
+
puts "Warning: GeoIP.dat database missing. Can't parse countries. #{GEOIP_PATH}"
|
42
|
+
geo = nil
|
43
|
+
end
|
44
|
+
reply = %x[gslist -p "#{GEOIP_PATH}" -n #{@game} #{geo}-X #{PARAMS.clone.map{|e| "#{DELIMIT}#{e}"}.join("")}]
|
45
|
+
reply.gsub!("\\\\\\", "") if geo
|
46
|
+
reply.split("\n")
|
47
|
+
end
|
48
|
+
|
49
|
+
RX_H = /\A([\.0-9]*):([0-9]*) *\\(.*)/
|
50
|
+
STR_SPLIT = "\\"
|
51
|
+
def to_hash(ar)
|
52
|
+
ar.each_with_index do |entry, index|
|
53
|
+
str = entry[RX_H]
|
54
|
+
next unless str
|
55
|
+
ip, port, content = $1, $2, $3
|
56
|
+
content = content.split(STR_SPLIT)
|
57
|
+
content << "" unless (content.size % 2 == 0)
|
58
|
+
i = 0
|
59
|
+
content.map! do |e|
|
60
|
+
i += 1
|
61
|
+
i % 2 == 0 ? e : clean(e)
|
62
|
+
end
|
63
|
+
addr = "#{ip}:#{port}"
|
64
|
+
if @list.has_key?(addr)
|
65
|
+
e = @list[addr]
|
66
|
+
else
|
67
|
+
e = Hash.new
|
68
|
+
e[:ip] = ip
|
69
|
+
e[:port] = port
|
70
|
+
@list[addr] = e
|
71
|
+
end
|
72
|
+
if e[:gamedata]
|
73
|
+
e[:gamedata].merge!(Hash[*content])
|
74
|
+
else
|
75
|
+
e[:gamedata] = Hash[*content]
|
76
|
+
end
|
77
|
+
end
|
78
|
+
@list
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,231 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
=begin
|
4
|
+
GameSpy parser class by Sickboy [Patrick Roza] (sb_at_dev-heaven.net)
|
5
|
+
|
6
|
+
Notes:
|
7
|
+
Gamedata values are not split, Player lists can be (names, teams, scores, deaths), while individual values still are not.
|
8
|
+
=end
|
9
|
+
|
10
|
+
require_relative 'base'
|
11
|
+
|
12
|
+
module GamespyQuery
|
13
|
+
class Parser
|
14
|
+
STR_X0 = "\x00"
|
15
|
+
STR_SPLIT = STR_X0
|
16
|
+
STR_EMPTY = ""
|
17
|
+
STR_ID = "\x00\x04\x05\x06\a"
|
18
|
+
|
19
|
+
RX_SPLITNUM = /^splitnum\x00(.)/i
|
20
|
+
RX_X0_E = /\x00$/
|
21
|
+
RX_PLAYER_HEADER = /\x01/
|
22
|
+
RX_END = /\x00\x02$/
|
23
|
+
|
24
|
+
# packets:
|
25
|
+
# - Hash, key: packetID, value: packetDATA
|
26
|
+
# or
|
27
|
+
# - Array, packetDATA ordered already by packetID
|
28
|
+
def initialize(packets)
|
29
|
+
@packets = case packets
|
30
|
+
when Hash
|
31
|
+
packets.keys.sort.map{|key| packets[key] }
|
32
|
+
when Array
|
33
|
+
packets
|
34
|
+
else
|
35
|
+
raise "Unsupported format"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# Returns Hash with parsed data (:game and :players)
|
40
|
+
# :game => Hash, Key: InfoKey, Value: InfoValue
|
41
|
+
# :players => Hash, Key: InfoType, Value: Array of Values
|
42
|
+
def parse
|
43
|
+
data = {}
|
44
|
+
data[:game] = {} # Key: InfoKey, Value: InfoValue
|
45
|
+
data[:players] = {} # Key: InfoType, Value: Array of Values
|
46
|
+
player_info = false
|
47
|
+
player_data = ""
|
48
|
+
|
49
|
+
# Parse the packets
|
50
|
+
@packets.each do |packet|
|
51
|
+
packet = clean_packet(packet)
|
52
|
+
|
53
|
+
if player_info
|
54
|
+
# Player header was found before, add packet to player_data
|
55
|
+
player_data += packet
|
56
|
+
else
|
57
|
+
if packet =~ RX_PLAYER_HEADER
|
58
|
+
# Found Player header, packet possibly contains partial gamedata too
|
59
|
+
player_info = true
|
60
|
+
packets = packet.split(RX_PLAYER_HEADER, 2)
|
61
|
+
|
62
|
+
# Parse last game_data piece if available
|
63
|
+
data[:game].merge!(parse_game_data(packets[0])) unless packets[0].empty?
|
64
|
+
|
65
|
+
# Collect the player_data if available
|
66
|
+
player_data += packets[1]
|
67
|
+
else
|
68
|
+
# GameData-only
|
69
|
+
data[:game].merge!(parse_game_data(packet))
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
# Parse player_data
|
75
|
+
unless player_data.empty?
|
76
|
+
data[:players] = parse_player_data(player_data)
|
77
|
+
end
|
78
|
+
|
79
|
+
data
|
80
|
+
end
|
81
|
+
|
82
|
+
if RUBY_PLATFORM =~ /mswin32/
|
83
|
+
def get_string(str)
|
84
|
+
System::Text::Encoding.UTF8.GetString(System::Array.of(System::Byte).new(str.bytes.to_a)).to_s
|
85
|
+
end
|
86
|
+
else
|
87
|
+
def get_string(str)
|
88
|
+
#(str + ' ').encode("UTF-8", :invalid => :replace, :undef => :replace)[0..-2]
|
89
|
+
str
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def clean_packet(packet)
|
94
|
+
packet = packet.clone
|
95
|
+
packet.sub!(STR_ID, STR_EMPTY) # Cut off the identity
|
96
|
+
packet.sub!(RX_SPLITNUM, STR_EMPTY) # Cut off the splitnum
|
97
|
+
packet.sub!(RX_X0_E, STR_EMPTY) # Cut off last \x00
|
98
|
+
packet.sub!(RX_X0_S, STR_EMPTY) # Cut off first \x00
|
99
|
+
packet.sub!(RX_END, STR_EMPTY) # Cut off the last \x00\x02
|
100
|
+
|
101
|
+
# Encoding
|
102
|
+
get_string(packet)
|
103
|
+
end
|
104
|
+
|
105
|
+
def parse_game_data(packet)
|
106
|
+
Tools.debug {"Game Parsing #{packet.inspect}"}
|
107
|
+
|
108
|
+
key = nil
|
109
|
+
game_data = {}
|
110
|
+
|
111
|
+
packet.split(STR_SPLIT).each_with_index do |data, index|
|
112
|
+
if (index % 2) == 0
|
113
|
+
key = data
|
114
|
+
else
|
115
|
+
game_data[key] = data
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
game_data
|
120
|
+
end
|
121
|
+
|
122
|
+
RX_PLAYER_EMPTY = /^player_\x00\x00\x00/
|
123
|
+
RX_PLAYER_INFO = /\x01(team|player|score|deaths)_.(.)/ # \x00 from previous packet, \x01 from continueing player info, (.) - should it overwrite previous value?
|
124
|
+
STR_DEATHS = "deaths_\x00\x00"
|
125
|
+
STR_PLAYER = "player_\x00\x00"
|
126
|
+
STR_TEAM = "team_\x00\x00"
|
127
|
+
STR_SCORE = "score_\x00\x00"
|
128
|
+
|
129
|
+
# TODO: Cleanup
|
130
|
+
def parse_player_data(packet)
|
131
|
+
Tools.debug {"Player Parsing #{packet.inspect}"}
|
132
|
+
|
133
|
+
player_data = {:names => [], :teams => [], :scores => [], :deaths => []} # [[], [], [], []]
|
134
|
+
|
135
|
+
return player_data if packet.nil? || packet.empty?
|
136
|
+
|
137
|
+
data = packet.clone
|
138
|
+
unless data =~ RX_PLAYER_EMPTY
|
139
|
+
|
140
|
+
# Leave out the character or Replace character with special string later used to replace the previous value
|
141
|
+
data.sub!(RX_PLAYER_INFO) { |r|
|
142
|
+
str = $1
|
143
|
+
if $2 == STR_X0
|
144
|
+
# If a proper primary info header of this type was not yet found, replace this secondary header with a proper primary header
|
145
|
+
# This will add the broken info header to the previous info list (name for team, team for score, score for deaths)
|
146
|
+
# However the resulting arrays are limited to num_players, so the info is discared anyway.
|
147
|
+
# TODO: Cleaner implementation!
|
148
|
+
data =~ /(^|[^\x01])#{str}_\x00\x00/ ? STR_X0 : :"#{str}_\x00\x00"
|
149
|
+
else
|
150
|
+
STR_SIX_X0
|
151
|
+
end
|
152
|
+
}
|
153
|
+
|
154
|
+
data, deaths = data.split(STR_DEATHS, 2)
|
155
|
+
data, scores = data.split(STR_SCORE, 2)
|
156
|
+
data, teams = data.split(STR_TEAM, 2)
|
157
|
+
data, names = data.split(STR_PLAYER, 2)
|
158
|
+
|
159
|
+
orig_data = [names, teams, scores, deaths]
|
160
|
+
|
161
|
+
# TODO: Handle seperate score
|
162
|
+
orig_data.each_with_index do |data, i|
|
163
|
+
next if data.nil? || data.empty?
|
164
|
+
str = data.clone
|
165
|
+
|
166
|
+
str.sub!(RX_X0_E, STR_EMPTY) # Remove last \x00
|
167
|
+
|
168
|
+
# Parse the data - \x00 is printed after a non-nil entry, otherwise \x00 means nil (e.g empty team)
|
169
|
+
until str.empty?
|
170
|
+
entry = str[RX_X0_SPEC]
|
171
|
+
player_data[player_data.keys[i]] << entry.sub(STR_X0, STR_EMPTY)
|
172
|
+
str.sub!(entry, STR_EMPTY)
|
173
|
+
end
|
174
|
+
|
175
|
+
# Search for SIX string to overwrite last entry
|
176
|
+
new_player_data = []
|
177
|
+
overwrite = false
|
178
|
+
player_data[player_data.keys[i]].each do |info|
|
179
|
+
if info == STR_SIX
|
180
|
+
overwrite = true # tag so that the next entry will overwrite the latest entry
|
181
|
+
next # ignore
|
182
|
+
else
|
183
|
+
if overwrite
|
184
|
+
new_player_data[-1] = info # Overwrite latest entry
|
185
|
+
overwrite = false # done the overwrite
|
186
|
+
else
|
187
|
+
#break if new_player_data.size == num_players
|
188
|
+
new_player_data << info # insert entry
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
192
|
+
player_data[player_data.keys[i]] = new_player_data
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
player_data
|
197
|
+
end
|
198
|
+
|
199
|
+
# Hash of Hashes
|
200
|
+
def self.pretty_player_data(data)
|
201
|
+
player_data = {}
|
202
|
+
|
203
|
+
data[:names].each_with_index do |name, index|
|
204
|
+
player_data[name] = {}
|
205
|
+
player_data[name][:team] = data[:teams][index]
|
206
|
+
player_data[name][:score] = data[:scores][index]
|
207
|
+
player_data[name][:deaths] = data[:deaths][index]
|
208
|
+
end
|
209
|
+
|
210
|
+
player_data
|
211
|
+
end
|
212
|
+
|
213
|
+
# Array of Hashes
|
214
|
+
def self.pretty_player_data2(data)
|
215
|
+
player_data = []
|
216
|
+
|
217
|
+
data[:names].each_with_index do |name, index|
|
218
|
+
player = {}
|
219
|
+
|
220
|
+
player[:name] = name
|
221
|
+
player[:team] = data[:teams][index]
|
222
|
+
player[:score] = data[:scores][index]
|
223
|
+
player[:deaths] = data[:deaths][index]
|
224
|
+
|
225
|
+
player_data << player
|
226
|
+
end
|
227
|
+
|
228
|
+
player_data
|
229
|
+
end
|
230
|
+
end
|
231
|
+
end
|
@@ -0,0 +1,268 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
# GameSpy query class by Sickboy [Patrick Roza] (sb_at_dev-heaven.net)
|
3
|
+
|
4
|
+
require 'yaml'
|
5
|
+
require_relative 'base'
|
6
|
+
require_relative 'parser'
|
7
|
+
|
8
|
+
module GamespyQuery
|
9
|
+
class Socket < Base
|
10
|
+
TIMEOUT = 3
|
11
|
+
MAX_PACKETS = 7
|
12
|
+
|
13
|
+
ID_PACKET = [0x04, 0x05, 0x06, 0x07].pack("c*") # TODO: Randomize
|
14
|
+
BASE_PACKET = [0xFE, 0xFD, 0x00].pack("c*")
|
15
|
+
CHALLENGE_PACKET = [0xFE, 0xFD, 0x09].pack("c*")
|
16
|
+
|
17
|
+
FULL_INFO_PACKET_MP = [0xFF, 0xFF, 0xFF, 0x01].pack("c*")
|
18
|
+
FULL_INFO_PACKET = [0xFF, 0xFF, 0xFF].pack("c*")
|
19
|
+
SERVER_INFO_PACKET = [0xFF, 0x00, 0x00].pack("c*")
|
20
|
+
PLAYER_INFO_PACKET = [0x00, 0xFF, 0x00].pack("c*")
|
21
|
+
|
22
|
+
STR_HOSTNAME = "hostname"
|
23
|
+
STR_PLAYERS = "players"
|
24
|
+
STR_DEATHS = "deaths_\x00\x00"
|
25
|
+
STR_PLAYER = "player_\x00\x00"
|
26
|
+
STR_TEAM = "team_\x00\x00"
|
27
|
+
STR_SCORE = "score_\x00\x00"
|
28
|
+
STR_X0, STR_X1, STR_X2 = "\x00", "\x01", "\x02"
|
29
|
+
SPLIT = STR_X0
|
30
|
+
STR_END = "\x00\x02"
|
31
|
+
STR_EMPTY = Tools::STR_EMPTY
|
32
|
+
STR_BLA = "%c%c%c%c".encode("ASCII-8BIT")
|
33
|
+
STR_GARBAGE = "\x00\x04\x05\x06\a"
|
34
|
+
STR_SIX = "$SIX_OVERWRITE_PREVIOUS$"
|
35
|
+
STR_SIX_X0 = "\x00#{STR_SIX}\x00"
|
36
|
+
|
37
|
+
RX_PLAYER_EMPTY = /^player_\x00\x00\x00/
|
38
|
+
RX_PLAYER_INFO = /\x01(team|player|score|deaths)_.(.)/ # \x00 from previous packet, \x01 from continueing player info, (.) - should it overwrite previous value?
|
39
|
+
RX_X0, RX_X0_S, RX_X0_E = /\x00/, /^\x00/, /\x00$/
|
40
|
+
RX_X0_SPEC = /^\x00|[^\x00]+\x00?/
|
41
|
+
|
42
|
+
RX_NO_CHALLENGE = /0@0$/
|
43
|
+
RX_CHALLENGE = /0@/
|
44
|
+
RX_CHALLENGE2 = /[^0-9\-]/si
|
45
|
+
RX_SPLITNUM = /^splitnum\x00(.)/i
|
46
|
+
|
47
|
+
def create_socket(*params)
|
48
|
+
puts "Creating socket #{params}"
|
49
|
+
_create_socket(*params)
|
50
|
+
end
|
51
|
+
|
52
|
+
def socket_send(*params)
|
53
|
+
puts "Sending socket #{params}"
|
54
|
+
_socket_send(*params)
|
55
|
+
end
|
56
|
+
|
57
|
+
def socket_receive(*params)
|
58
|
+
puts "Receiving socket #{params}"
|
59
|
+
_socket_receive(*params)
|
60
|
+
end
|
61
|
+
|
62
|
+
def socket_close(*params)
|
63
|
+
puts "Closing socket #{params}"
|
64
|
+
_socket_close(*params)
|
65
|
+
end
|
66
|
+
|
67
|
+
def get_string(*params)
|
68
|
+
puts "Getting string #{params}"
|
69
|
+
_get_string(*params)
|
70
|
+
end
|
71
|
+
|
72
|
+
if RUBY_PLATFORM =~ /mswin32/
|
73
|
+
include System::Net
|
74
|
+
include System::Net::Sockets
|
75
|
+
|
76
|
+
def get_string(str)
|
77
|
+
str.map {|e| e.chr}.join # begin; System::Text::Encoding.USASCII.GetString(reply[0]).to_s; rescue nil, Exception => e; Tools.log_exception(e); reply[0].map {|e| e.chr}.join; end
|
78
|
+
end
|
79
|
+
|
80
|
+
def _create_socket(host, port)
|
81
|
+
@ip_end_point = IPEndPoint.new(IPAddress.Any, 0)
|
82
|
+
@s = UdpClient.new
|
83
|
+
@s.client.receive_timeout = TIMEOUT * 1000
|
84
|
+
@s.connect(host, port.to_i)
|
85
|
+
end
|
86
|
+
|
87
|
+
def _socket_send(packet)
|
88
|
+
@s.Send(packet, packet.length)
|
89
|
+
end
|
90
|
+
|
91
|
+
def _socket_receive
|
92
|
+
@s.Receive(@ip_end_point)
|
93
|
+
end
|
94
|
+
|
95
|
+
def _socket_close
|
96
|
+
@s.close
|
97
|
+
end
|
98
|
+
else
|
99
|
+
require 'socket'
|
100
|
+
require 'timeout'
|
101
|
+
|
102
|
+
def get_string(str)
|
103
|
+
str
|
104
|
+
end
|
105
|
+
|
106
|
+
def _create_socket(host, port)
|
107
|
+
@s = UDPSocket.new
|
108
|
+
@s.connect(host, port)
|
109
|
+
end
|
110
|
+
|
111
|
+
def _socket_send(packet)
|
112
|
+
@s.puts(packet)
|
113
|
+
end
|
114
|
+
|
115
|
+
def _socket_receive
|
116
|
+
begin
|
117
|
+
Timeout::timeout(TIMEOUT) do
|
118
|
+
@s.recvfrom(4096)
|
119
|
+
end
|
120
|
+
rescue Timeout::Error
|
121
|
+
#socket_close
|
122
|
+
raise TimeoutError
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def _socket_close
|
127
|
+
@s.close
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
attr_accessor :silent
|
132
|
+
def initialize(host, port, silent = nil)
|
133
|
+
@host, @port, @silent = host, port, silent
|
134
|
+
end
|
135
|
+
|
136
|
+
# Supports challenge/response and multi-packet
|
137
|
+
def sync
|
138
|
+
game_data, key, reply = {}, nil, self.fetch
|
139
|
+
return game_data if reply.nil?
|
140
|
+
|
141
|
+
parser = Parser.new(reply)
|
142
|
+
data = parser.parse
|
143
|
+
|
144
|
+
game_data.merge!(data[:game])
|
145
|
+
game_data["players"] = Parser.pretty_player_data2(data[:players]).sort {|a, b| a[:name].downcase <=> b[:name].downcase }
|
146
|
+
|
147
|
+
game_data["ping"] = @ping unless @ping.nil?
|
148
|
+
|
149
|
+
game_data
|
150
|
+
end
|
151
|
+
|
152
|
+
def fetch
|
153
|
+
data = {}
|
154
|
+
status, reply = nil, nil
|
155
|
+
|
156
|
+
# Prepare socket / endpoint and connect
|
157
|
+
create_socket(@host, @port)
|
158
|
+
|
159
|
+
# Prepare and send challenge request
|
160
|
+
# TODO: Randomize
|
161
|
+
id_packet = ID_PACKET
|
162
|
+
packet = CHALLENGE_PACKET + id_packet
|
163
|
+
Tools.debug{"Sending Challenge (#{packet.length}): #{packet.inspect}"}
|
164
|
+
sent = Time.now
|
165
|
+
|
166
|
+
socket_send(packet)
|
167
|
+
|
168
|
+
pings = []
|
169
|
+
|
170
|
+
challenge, received = nil, nil
|
171
|
+
begin
|
172
|
+
# By default, Blocks until a message returns on this socket from a remote host.
|
173
|
+
reply = socket_receive
|
174
|
+
received = Time.now
|
175
|
+
# TODO: Improve ping test?
|
176
|
+
ping = received - sent
|
177
|
+
pings << ping
|
178
|
+
Tools.debug {"PingTest: #{ping}"}
|
179
|
+
challenge = reply[0]
|
180
|
+
rescue nil, Exception => e # Cannot use ensure as we want to keep the socket open :P
|
181
|
+
socket_close
|
182
|
+
raise e
|
183
|
+
end
|
184
|
+
return nil if challenge.nil? || challenge.empty?
|
185
|
+
|
186
|
+
# Prepare challenge response, if needed
|
187
|
+
str = get_string(challenge)
|
188
|
+
Tools.debug{"Received challenge response (#{str.length}): #{str.inspect}"}
|
189
|
+
need_challenge = !(str.sub(STR_X0, STR_EMPTY) =~ RX_NO_CHALLENGE)
|
190
|
+
|
191
|
+
if need_challenge
|
192
|
+
Tools.debug {"Needs challenge!"}
|
193
|
+
str = str.sub(RX_CHALLENGE, STR_EMPTY).gsub(RX_CHALLENGE2, STR_EMPTY).to_i
|
194
|
+
challenge_packet = sprintf(STR_BLA, handle_chr(str >> 24), handle_chr(str >> 16), handle_chr(str >> 8), handle_chr(str >> 0))
|
195
|
+
end
|
196
|
+
|
197
|
+
# Prepare and send info request packet
|
198
|
+
packet = need_challenge ? BASE_PACKET + id_packet + challenge_packet + FULL_INFO_PACKET_MP : BASE_PACKET + id_packet + FULL_INFO_PACKET_MP
|
199
|
+
Tools.debug{"Sending:\n#{packet.inspect}"}
|
200
|
+
sent = Time.now
|
201
|
+
socket_send(packet)
|
202
|
+
|
203
|
+
# Receive response to info request packet, up to 7 packets of information, each limited to 1400 bytes
|
204
|
+
max_packets = MAX_PACKETS # Default max
|
205
|
+
begin
|
206
|
+
# In case some server info didn't fit in a single packet, there will be no proper END OF DATA signal
|
207
|
+
# So we manually quit after reaching MAX_PACKETS.
|
208
|
+
until data.size >= max_packets
|
209
|
+
reply = socket_receive
|
210
|
+
|
211
|
+
if data.empty?
|
212
|
+
received = Time.now
|
213
|
+
ping = received - sent
|
214
|
+
pings << ping
|
215
|
+
Tools.debug {"PingTest: #{ping}"}
|
216
|
+
end
|
217
|
+
index = 0
|
218
|
+
|
219
|
+
game_data = get_string(reply[0])
|
220
|
+
Tools.debug {"Received (#{data.size + 1}):\n\n#{game_data.inspect}\n\n#{game_data}\n\n"}
|
221
|
+
|
222
|
+
if game_data.sub(STR_GARBAGE, STR_EMPTY)[RX_SPLITNUM]
|
223
|
+
splitnum = $1
|
224
|
+
flag = splitnum.unpack("C")[0]
|
225
|
+
index = (flag & 127).to_i
|
226
|
+
last = flag & 0x80 > 0
|
227
|
+
# Data could be received out of order, use the "index" id when "last" flag is true, to determine total packet_count
|
228
|
+
max_packets = index + 1 if last # update the max
|
229
|
+
Tools.debug {"Splitnum: #{splitnum.inspect} (#{splitnum}) (#{flag}, #{index}, #{last}) Max: #{max_packets}"}
|
230
|
+
else
|
231
|
+
max_packets = 1
|
232
|
+
end
|
233
|
+
data[index] = game_data #.sub(RX_X0_S, STR_EMPTY) # Cut off first \x00 from package
|
234
|
+
end
|
235
|
+
ensure
|
236
|
+
socket_close
|
237
|
+
end
|
238
|
+
|
239
|
+
pings.map!{|ping| (ping * 1000).round}
|
240
|
+
pings_c = 0
|
241
|
+
pings.each { |ping| pings_c += ping }
|
242
|
+
|
243
|
+
ping = pings_c / pings.size
|
244
|
+
Tools.debug{"Gamespy pings: #{pings}, #{ping}"}
|
245
|
+
|
246
|
+
return nil if data.keys.empty?
|
247
|
+
@ping = ping
|
248
|
+
data.each_pair {|k, d| Tools.debug {"GSPY Infos: #{k} #{d.size}"} } unless @silent || !$debug
|
249
|
+
|
250
|
+
data
|
251
|
+
end
|
252
|
+
|
253
|
+
def handle_chr(number)
|
254
|
+
number = ((number % 256)+256) if number < 0
|
255
|
+
number = number % 256 if number > 255
|
256
|
+
number
|
257
|
+
end
|
258
|
+
end
|
259
|
+
end
|
260
|
+
|
261
|
+
if $0 == __FILE__
|
262
|
+
host = ARGV[0]
|
263
|
+
port = ARGV[1]
|
264
|
+
g = Six::Query::Gamespy.new(host, port)
|
265
|
+
r = g.sync
|
266
|
+
exit unless r
|
267
|
+
puts r.to_yaml
|
268
|
+
end
|
metadata
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: gamespy_query
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Patrick Roza
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-01-06 00:00:00.000000000 Z
|
13
|
+
dependencies: []
|
14
|
+
description: ''
|
15
|
+
email:
|
16
|
+
- sb@dev-heaven.net
|
17
|
+
executables: []
|
18
|
+
extensions: []
|
19
|
+
extra_rdoc_files: []
|
20
|
+
files:
|
21
|
+
- .gitignore
|
22
|
+
- Gemfile
|
23
|
+
- Rakefile
|
24
|
+
- gamespy_query.gemspec
|
25
|
+
- lib/gamespy_query.rb
|
26
|
+
- lib/gamespy_query/base.rb
|
27
|
+
- lib/gamespy_query/master.rb
|
28
|
+
- lib/gamespy_query/parser.rb
|
29
|
+
- lib/gamespy_query/socket.rb
|
30
|
+
- lib/gamespy_query/version.rb
|
31
|
+
homepage: http://dev-heaven.net
|
32
|
+
licenses: []
|
33
|
+
post_install_message:
|
34
|
+
rdoc_options: []
|
35
|
+
require_paths:
|
36
|
+
- lib
|
37
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
38
|
+
none: false
|
39
|
+
requirements:
|
40
|
+
- - ! '>='
|
41
|
+
- !ruby/object:Gem::Version
|
42
|
+
version: '0'
|
43
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
44
|
+
none: false
|
45
|
+
requirements:
|
46
|
+
- - ! '>='
|
47
|
+
- !ruby/object:Gem::Version
|
48
|
+
version: '0'
|
49
|
+
requirements: []
|
50
|
+
rubyforge_project: gamespy_query
|
51
|
+
rubygems_version: 1.8.13
|
52
|
+
signing_key:
|
53
|
+
specification_version: 3
|
54
|
+
summary: Ruby library for accessing Gamespy services
|
55
|
+
test_files: []
|