gamespy_query 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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: []
|