ruby-qstat 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/.gitignore +5 -0
- data/LICENSE +1 -0
- data/README.rdoc +20 -0
- data/Rakefile +56 -0
- data/VERSION +1 -0
- data/lib/ruby-qstat.rb +474 -0
- data/test/ruby-qstat_test.rb +7 -0
- data/test/test_helper.rb +10 -0
- metadata +112 -0
data/.document
ADDED
data/LICENSE
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
Copyright (c) 2012 kimoto
|
data/README.rdoc
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
= ruby-qstat
|
2
|
+
QStat Ruby Frontend (Real-time game server stat fetcher)
|
3
|
+
|
4
|
+
= Require
|
5
|
+
- Ruby1.9
|
6
|
+
- QStat 2.12
|
7
|
+
|
8
|
+
= Install
|
9
|
+
gem install ruby-qstat
|
10
|
+
|
11
|
+
= Example
|
12
|
+
Update server list from hl2master server
|
13
|
+
QStat.query_serverlist("hl2master.steampowered.com:27011", "stm", "left4dead2", maxping = 100)
|
14
|
+
|
15
|
+
Update server
|
16
|
+
QStat.query("xxx.yyy.zzz.qqq", "a2s")
|
17
|
+
|
18
|
+
Update server (information only)
|
19
|
+
QStat.query_serverinfo("xxx.yyy.zzz.qqq", "a2s")
|
20
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'jeweler'
|
6
|
+
Jeweler::Tasks.new do |gem|
|
7
|
+
gem.name = "ruby-qstat"
|
8
|
+
gem.summary = %Q{QStat Ruby Frontend (Real-time game server stat fetcher)}
|
9
|
+
gem.description = %Q{TODO: longer description of your gem}
|
10
|
+
gem.email = "sub+peerler@gmail.com"
|
11
|
+
gem.homepage = "http://github.com/kimoto/ruby-qstat"
|
12
|
+
gem.authors = ["kimoto"]
|
13
|
+
gem.add_development_dependency "thoughtbot-shoulda"
|
14
|
+
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
|
15
|
+
end
|
16
|
+
rescue LoadError
|
17
|
+
puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
|
18
|
+
end
|
19
|
+
|
20
|
+
require 'rake/testtask'
|
21
|
+
Rake::TestTask.new(:test) do |test|
|
22
|
+
test.libs << 'lib' << 'test'
|
23
|
+
test.pattern = 'test/**/*_test.rb'
|
24
|
+
test.verbose = true
|
25
|
+
end
|
26
|
+
|
27
|
+
begin
|
28
|
+
require 'rcov/rcovtask'
|
29
|
+
Rcov::RcovTask.new do |test|
|
30
|
+
test.libs << 'test'
|
31
|
+
test.pattern = 'test/**/*_test.rb'
|
32
|
+
test.verbose = true
|
33
|
+
end
|
34
|
+
rescue LoadError
|
35
|
+
task :rcov do
|
36
|
+
abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
task :test => :check_dependencies
|
41
|
+
|
42
|
+
task :default => :test
|
43
|
+
|
44
|
+
require 'rake/rdoctask'
|
45
|
+
Rake::RDocTask.new do |rdoc|
|
46
|
+
if File.exist?('VERSION')
|
47
|
+
version = File.read('VERSION')
|
48
|
+
else
|
49
|
+
version = ""
|
50
|
+
end
|
51
|
+
|
52
|
+
rdoc.rdoc_dir = 'rdoc'
|
53
|
+
rdoc.title = "ruby-qstat #{version}"
|
54
|
+
rdoc.rdoc_files.include('README*')
|
55
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
56
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.1.1
|
data/lib/ruby-qstat.rb
ADDED
@@ -0,0 +1,474 @@
|
|
1
|
+
#-*- coding: utf-8 -*-
|
2
|
+
require 'open3'
|
3
|
+
require 'nokogiri'
|
4
|
+
require 'active_support'
|
5
|
+
require 'kconv'
|
6
|
+
require 'logger'
|
7
|
+
|
8
|
+
class String
|
9
|
+
def force_convert_to(from, to=nil)
|
10
|
+
if to.nil?
|
11
|
+
to = from
|
12
|
+
end
|
13
|
+
self.encode!("UTF-16BE", from, :invalid => :replace, :undef => :replace, :replace => '?')
|
14
|
+
self.encode!(to)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
class QStat
|
19
|
+
DEFAULT_MAX_PING = 50
|
20
|
+
|
21
|
+
@@qstat_path = "qstat"
|
22
|
+
@@logger = Logger.new(nil)
|
23
|
+
|
24
|
+
class Response
|
25
|
+
attr_accessor :address
|
26
|
+
attr_accessor :xml
|
27
|
+
attr_accessor :doc
|
28
|
+
attr_accessor :gametype
|
29
|
+
attr_accessor :gamename
|
30
|
+
|
31
|
+
def status_code
|
32
|
+
server = @doc.search("/qstat/server[1]").first
|
33
|
+
if server.attributes["type"].value.upcase == @gametype.upcase
|
34
|
+
if server.attributes["address"].value == @address
|
35
|
+
return server.attributes["status"].value
|
36
|
+
end
|
37
|
+
end
|
38
|
+
nil
|
39
|
+
end
|
40
|
+
|
41
|
+
def valid?
|
42
|
+
case status_code
|
43
|
+
when "UP"
|
44
|
+
true
|
45
|
+
else
|
46
|
+
false
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def to_s
|
51
|
+
@xml
|
52
|
+
end
|
53
|
+
|
54
|
+
def to_ror
|
55
|
+
Hash.from_xml(@doc.to_s)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
class ServerInfo
|
60
|
+
attr_accessor :addr
|
61
|
+
attr_accessor :num_players
|
62
|
+
attr_accessor :map
|
63
|
+
attr_accessor :response
|
64
|
+
attr_accessor :time
|
65
|
+
attr_accessor :server_name
|
66
|
+
attr_accessor :players
|
67
|
+
attr_accessor :game_type
|
68
|
+
|
69
|
+
attr_accessor :ping
|
70
|
+
attr_accessor :retries
|
71
|
+
attr_accessor :number_of_max_players
|
72
|
+
attr_accessor :number_of_players
|
73
|
+
attr_accessor :status
|
74
|
+
|
75
|
+
attr_accessor :rules
|
76
|
+
|
77
|
+
class Rule
|
78
|
+
attr_accessor :protocol
|
79
|
+
attr_accessor :gamedir
|
80
|
+
attr_accessor :gamename
|
81
|
+
attr_accessor :bots
|
82
|
+
attr_accessor :dedicated
|
83
|
+
attr_accessor :sv_os
|
84
|
+
attr_accessor :secure
|
85
|
+
attr_accessor :version
|
86
|
+
attr_accessor :game_port
|
87
|
+
attr_accessor :game_tag
|
88
|
+
|
89
|
+
def initialize
|
90
|
+
end
|
91
|
+
|
92
|
+
def self.create_from_xml(doc)
|
93
|
+
rule = Rule.new
|
94
|
+
rule.protocol = doc.search("rule[name=protocol]").text
|
95
|
+
rule.gamedir = doc.search("rule[name=gamedir]").text
|
96
|
+
rule.gamename = doc.search("rule[name=gamename]").text
|
97
|
+
rule.bots = doc.search("rule[name=bots]").text
|
98
|
+
rule.dedicated = doc.search("rule[name=dedicated]").text
|
99
|
+
rule.sv_os = doc.search("rule[name=sv_os]").text
|
100
|
+
rule.secure = doc.search("rule[name=secure]").text
|
101
|
+
rule.version = doc.search("rule[name=version]").text
|
102
|
+
rule.game_port = doc.search("rule[name=game_port]").text
|
103
|
+
rule.game_tag = doc.search("rule[name=game_tag]").text
|
104
|
+
rule
|
105
|
+
end
|
106
|
+
|
107
|
+
def game_tags
|
108
|
+
if @game_tag =~ /^.*\n@(.*)$/
|
109
|
+
$1.split(",")
|
110
|
+
else
|
111
|
+
[]
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def initialize(data=nil)
|
117
|
+
if data
|
118
|
+
parse_qstat_query_player(data)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
def self.create_from_xml(doc)
|
123
|
+
info = self.new
|
124
|
+
info.addr = doc.search("/server/hostname").text
|
125
|
+
info.server_name = doc.search("/server/name").text
|
126
|
+
info.game_type = doc.search("/server/gametype").text
|
127
|
+
info.map = doc.search("/server/map").text
|
128
|
+
info.num_players = doc.search("/server/numplayers").text
|
129
|
+
info.number_of_max_players = doc.search("/server/maxplayers").text
|
130
|
+
info.ping = doc.search("/server/ping").text
|
131
|
+
info.retries = doc.search("/server/retries").text
|
132
|
+
info.rules = []
|
133
|
+
doc.search("/server/rules").each{ |rule|
|
134
|
+
info.rules << Rule.create_from_xml(rule)
|
135
|
+
}
|
136
|
+
info.players = []
|
137
|
+
doc.search("/server/players/player").each{ |player|
|
138
|
+
info.players << PlayerInfo.create_from_xml(player)
|
139
|
+
}
|
140
|
+
info.number_of_players = [info.players.size, info.number_of_max_players].join(" / ")
|
141
|
+
info.status = doc.search("/server").first.attributes["status"].value
|
142
|
+
info
|
143
|
+
end
|
144
|
+
|
145
|
+
def parse_qstat_query_player(data)
|
146
|
+
lines = data.split(/\n/)
|
147
|
+
@header = lines[0]
|
148
|
+
@body = lines[1..-1]
|
149
|
+
@players = []
|
150
|
+
@rules = []
|
151
|
+
|
152
|
+
if no_response? or down?
|
153
|
+
return nil
|
154
|
+
end
|
155
|
+
|
156
|
+
if @header =~ /^(\d+(?:.\d+){3}:\d+)\s+(\d+\/\d+)\s+(\d+\/\d+)\s+(.*?)\s+([\d\s]+\/\s+\d+)\s+\S+\s+(.*)$/
|
157
|
+
@addr = $1
|
158
|
+
@num_players = $2
|
159
|
+
@map = $4
|
160
|
+
@response = $5
|
161
|
+
@server_name = $6
|
162
|
+
|
163
|
+
if @body
|
164
|
+
if @body.first =~ /^\t\s*\d+\s*frags.*$/
|
165
|
+
@body.each{ |player_info_line|
|
166
|
+
@players << PlayerInfo.new(player_info_line)
|
167
|
+
}
|
168
|
+
else
|
169
|
+
server_info = @body.join("\n")
|
170
|
+
server_info.force_convert_to('UTF-8')
|
171
|
+
server_info.sub!(/^\s+/, "")
|
172
|
+
server_info.gsub!(/\u0001/, "")
|
173
|
+
hash = {}
|
174
|
+
ary = server_info.split(",").map{|e| e.split("=")}
|
175
|
+
ary.each{ |e|
|
176
|
+
hash[e.first] = e.last
|
177
|
+
}
|
178
|
+
rule = Rule.new
|
179
|
+
rule.protocol = hash["protocol"]
|
180
|
+
rule.gamedir = hash["gamedir"]
|
181
|
+
rule.gamename = hash["gamename"]
|
182
|
+
rule.bots = hash["bots"]
|
183
|
+
rule.dedicated = hash["dedicated"]
|
184
|
+
rule.sv_os = hash["sv_os"]
|
185
|
+
rule.secure = hash["secure"]
|
186
|
+
rule.version = hash["version"]
|
187
|
+
rule.game_port = hash["game_port"]
|
188
|
+
rule.game_tag = hash["game_tag"]
|
189
|
+
p rule.game_tags
|
190
|
+
@rules << rule
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
@ping = parse_map_info.first
|
196
|
+
@number_of_max_players = parse_number_of_players.last
|
197
|
+
@number_of_players = [number_of_active_players, @number_of_max_players].join(" / ")
|
198
|
+
end
|
199
|
+
|
200
|
+
def number_of_active_players
|
201
|
+
@players.size
|
202
|
+
end
|
203
|
+
|
204
|
+
def no_response?
|
205
|
+
@header =~ /^(\d+(?:.\d+){3}:\d+)\s+no response$/
|
206
|
+
end
|
207
|
+
|
208
|
+
def down?
|
209
|
+
@header =~ /^(\d+(?:.\d+){3}:\d+)\s+DOWN$/
|
210
|
+
end
|
211
|
+
|
212
|
+
def playing_time
|
213
|
+
if empty_server?
|
214
|
+
return "00:00"
|
215
|
+
end
|
216
|
+
longest_playing_player.time_to_s
|
217
|
+
end
|
218
|
+
|
219
|
+
def playing_time_seconds
|
220
|
+
if empty_server?
|
221
|
+
return 0
|
222
|
+
end
|
223
|
+
longest_playing_player.time_to_i
|
224
|
+
end
|
225
|
+
|
226
|
+
def longest_playing_player
|
227
|
+
@players.max_by{ |player|
|
228
|
+
player.time_to_i
|
229
|
+
}
|
230
|
+
end
|
231
|
+
|
232
|
+
def empty_server?
|
233
|
+
@players.empty?
|
234
|
+
end
|
235
|
+
|
236
|
+
def suggest_game_type
|
237
|
+
unless @game_type.nil?
|
238
|
+
return @game_type
|
239
|
+
end
|
240
|
+
|
241
|
+
if (not @rules.empty?) and (not @rules.first.game_tags.empty?)
|
242
|
+
return @rules.first.game_tags.first
|
243
|
+
end
|
244
|
+
|
245
|
+
return 'unknown'
|
246
|
+
end
|
247
|
+
|
248
|
+
private
|
249
|
+
def parse_number_of_players
|
250
|
+
@num_players.gsub(" ", "").split("/")
|
251
|
+
end
|
252
|
+
|
253
|
+
def parse_map_info
|
254
|
+
@response.gsub(" ", "").split("/")
|
255
|
+
end
|
256
|
+
end
|
257
|
+
|
258
|
+
class PlayerInfo
|
259
|
+
attr_accessor :name
|
260
|
+
attr_accessor :frags
|
261
|
+
attr_accessor :time
|
262
|
+
|
263
|
+
def initialize(player_info_line=nil)
|
264
|
+
if player_info_line.nil?
|
265
|
+
return self
|
266
|
+
end
|
267
|
+
|
268
|
+
if player_info_line =~ /^\s+(\S+)\s+(\S+)\s+(.*?s)\s+(.*)$/
|
269
|
+
@frags = $1
|
270
|
+
@time = $3
|
271
|
+
@name = $4
|
272
|
+
self
|
273
|
+
else
|
274
|
+
raise "not found playerinfo structure: #{player_info_line}"
|
275
|
+
end
|
276
|
+
end
|
277
|
+
|
278
|
+
def self.create_from_xml(xml)
|
279
|
+
player_info = self.new
|
280
|
+
player_info.name = xml.search("./name").text
|
281
|
+
player_info.time = xml.search("./time").text
|
282
|
+
player_info.frags = xml.search("./frags").text
|
283
|
+
player_info
|
284
|
+
end
|
285
|
+
|
286
|
+
def time_to_i
|
287
|
+
qstat_timestr_to_seconds(@time)
|
288
|
+
end
|
289
|
+
|
290
|
+
def time_to_s
|
291
|
+
(minutes, seconds) = time_to_i.divmod(60)
|
292
|
+
(hours, minutes) = minutes.divmod(60)
|
293
|
+
"%02d:%02d" % [hours, minutes]
|
294
|
+
end
|
295
|
+
|
296
|
+
def to_s
|
297
|
+
"#<#{@name} #{time_to_s}>"
|
298
|
+
end
|
299
|
+
|
300
|
+
private
|
301
|
+
def qstat_timestr_to_seconds(timestr)
|
302
|
+
if timestr =~ /^\s*(\d+)h\s*(\d+)m\s*(\d+)s\s*$/
|
303
|
+
hours = $1.to_i
|
304
|
+
min = $2.to_i
|
305
|
+
sec = $3.to_i
|
306
|
+
elsif timestr =~ /^\s*(\d+)m\s*(\d+)s\s*$/
|
307
|
+
hours = 0
|
308
|
+
min = $1.to_i
|
309
|
+
sec = $2.to_i
|
310
|
+
elsif timestr =~ /^\s*(\d+)s\s*$/
|
311
|
+
hours = min = 0
|
312
|
+
sec = $1.to_i
|
313
|
+
elsif timestr =~ /^\s*(\d+)\s*$/
|
314
|
+
hours = min = 0
|
315
|
+
sec = $1.to_i
|
316
|
+
end
|
317
|
+
(hours * 60 * 60) + (min * 60) + sec
|
318
|
+
end
|
319
|
+
end
|
320
|
+
|
321
|
+
def initialize
|
322
|
+
raise "not implemented"
|
323
|
+
end
|
324
|
+
|
325
|
+
def self.qstat_path=(path)
|
326
|
+
@@qstat_path = path
|
327
|
+
end
|
328
|
+
|
329
|
+
def self.logger=(logger)
|
330
|
+
@@logger = logger
|
331
|
+
end
|
332
|
+
|
333
|
+
def self.query(host, gametype) # query player info
|
334
|
+
self.exec_qstat_query_cmd "#{@@qstat_path} -P -utf8 -nh -#{gametype} #{host}"
|
335
|
+
end
|
336
|
+
def self.server_info(*args)
|
337
|
+
self.query(*args)
|
338
|
+
end
|
339
|
+
|
340
|
+
def self.query_serverinfo(host, gametype) # query server info
|
341
|
+
self.exec_qstat_query_cmd "#{@@qstat_path} -R -utf8 -nh -#{gametype} #{host}"
|
342
|
+
end
|
343
|
+
|
344
|
+
def self.query_serverlist(host, gametype, gamename, maxping=DEFAULT_MAX_PING)
|
345
|
+
self.qslist(host, gametype, gamename, maxping){ |response|
|
346
|
+
if response.valid?
|
347
|
+
servers = response.doc.search("/qstat/server").to_a
|
348
|
+
if servers.size <= 1
|
349
|
+
raise "broken response" # 1件以下はおかしいっす
|
350
|
+
end
|
351
|
+
|
352
|
+
# 最初のserverは構造情報なので除去
|
353
|
+
servers.shift
|
354
|
+
|
355
|
+
# からじゃないよね??
|
356
|
+
if servers.empty?
|
357
|
+
raise "broken response#2"
|
358
|
+
end
|
359
|
+
|
360
|
+
# 取得したすべてのサーバー情報に対して
|
361
|
+
# ServerInfo化していく
|
362
|
+
infos = []
|
363
|
+
servers.each{ |server|
|
364
|
+
info = ServerInfo.create_from_xml(Nokogiri(server.to_s))
|
365
|
+
infos << info
|
366
|
+
}
|
367
|
+
infos
|
368
|
+
else
|
369
|
+
raise "response is invalid: #{response.inspect}"
|
370
|
+
end
|
371
|
+
}
|
372
|
+
end
|
373
|
+
|
374
|
+
def self.read_from_xml(path)
|
375
|
+
doc = Nokogiri(File.read(path))
|
376
|
+
servers = doc.search("/qstat/server").to_a
|
377
|
+
if servers.size <= 1
|
378
|
+
raise "broken response" # 1件以下はおかしいっす
|
379
|
+
end
|
380
|
+
|
381
|
+
# 最初のserverは構造情報なので除去
|
382
|
+
servers.shift
|
383
|
+
|
384
|
+
# からじゃないよね??
|
385
|
+
if servers.empty?
|
386
|
+
raise "broken response#2"
|
387
|
+
end
|
388
|
+
|
389
|
+
# 取得したすべてのサーバー情報に対して
|
390
|
+
# ServerInfo化していく
|
391
|
+
infos = []
|
392
|
+
servers.each{ |server|
|
393
|
+
info = ServerInfo.create_from_xml(Nokogiri(server.to_s))
|
394
|
+
infos << info
|
395
|
+
}
|
396
|
+
infos
|
397
|
+
end
|
398
|
+
|
399
|
+
def self.qslist(host, gametype, gamename, maxping=DEFAULT_MAX_PING, &block)
|
400
|
+
res = Response.new
|
401
|
+
res.address = host
|
402
|
+
res.gametype = gametype
|
403
|
+
res.gamename = gamename
|
404
|
+
|
405
|
+
broken_xml = self.get_xml(host, gametype, gamename){ |line|
|
406
|
+
if line =~ /<ping>(\d+)<\/ping>/
|
407
|
+
ping = Regexp.last_match(1).to_i
|
408
|
+
if maxping < ping
|
409
|
+
true
|
410
|
+
end
|
411
|
+
end
|
412
|
+
}
|
413
|
+
|
414
|
+
doc = Nokogiri(broken_xml)
|
415
|
+
doc.search("/qstat/server/ping").each{ |ping_tag|
|
416
|
+
if ping_tag.text.to_i >= maxping
|
417
|
+
ping_tag.parent.remove
|
418
|
+
end
|
419
|
+
}
|
420
|
+
res.doc = doc
|
421
|
+
res.xml = doc.to_s
|
422
|
+
|
423
|
+
if block_given?
|
424
|
+
block.call(res)
|
425
|
+
else
|
426
|
+
res
|
427
|
+
end
|
428
|
+
end
|
429
|
+
def self.qstat(*args, &block)
|
430
|
+
self.qslist(*args, &block)
|
431
|
+
end
|
432
|
+
|
433
|
+
## Low API
|
434
|
+
def self.exec_qstat_query_cmd(cmd_str)
|
435
|
+
ServerInfo.new self.exec_cmd(cmd_str).force_convert_to("UTF-8")
|
436
|
+
end
|
437
|
+
|
438
|
+
def self.exec_cmd(*params, &filter)
|
439
|
+
data = ""
|
440
|
+
Open3.popen3(*params){ |i,o,e,w|
|
441
|
+
i.close_write
|
442
|
+
|
443
|
+
tl = Thread.new{
|
444
|
+
begin
|
445
|
+
while line = o.gets
|
446
|
+
if block_given? and filter.call(line)
|
447
|
+
data += line
|
448
|
+
Process.kill :TERM, w.pid
|
449
|
+
break
|
450
|
+
end
|
451
|
+
data += line
|
452
|
+
end
|
453
|
+
rescue
|
454
|
+
@@logger.error $!
|
455
|
+
end
|
456
|
+
}
|
457
|
+
tl2 = Thread.new{
|
458
|
+
begin
|
459
|
+
while !e.eof?
|
460
|
+
@@logger.info e.read 1024
|
461
|
+
end
|
462
|
+
rescue
|
463
|
+
@@logger.error $!
|
464
|
+
end
|
465
|
+
}
|
466
|
+
tl.join
|
467
|
+
}
|
468
|
+
data
|
469
|
+
end
|
470
|
+
|
471
|
+
def self.get_xml(host, gametype, gamename, &filter)
|
472
|
+
self.exec_cmd("#{@@qstat_path} -utf8 -xml -P -R -nh -#{gametype},game=#{gamename} #{host}", &filter)
|
473
|
+
end
|
474
|
+
end
|
data/test/test_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,112 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: ruby-qstat
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 0
|
7
|
+
- 1
|
8
|
+
- 1
|
9
|
+
version: 0.1.1
|
10
|
+
platform: ruby
|
11
|
+
authors:
|
12
|
+
- kimoto
|
13
|
+
autorequire:
|
14
|
+
bindir: bin
|
15
|
+
cert_chain: []
|
16
|
+
|
17
|
+
date: 2012-02-04 00:00:00 +09:00
|
18
|
+
default_executable:
|
19
|
+
dependencies:
|
20
|
+
- !ruby/object:Gem::Dependency
|
21
|
+
name: nokogiri
|
22
|
+
prerelease: false
|
23
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
24
|
+
none: false
|
25
|
+
requirements:
|
26
|
+
- - ">="
|
27
|
+
- !ruby/object:Gem::Version
|
28
|
+
segments:
|
29
|
+
- 0
|
30
|
+
version: "0"
|
31
|
+
type: :runtime
|
32
|
+
version_requirements: *id001
|
33
|
+
- !ruby/object:Gem::Dependency
|
34
|
+
name: active_support
|
35
|
+
prerelease: false
|
36
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
37
|
+
none: false
|
38
|
+
requirements:
|
39
|
+
- - ">="
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
segments:
|
42
|
+
- 0
|
43
|
+
version: "0"
|
44
|
+
type: :runtime
|
45
|
+
version_requirements: *id002
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: thoughtbot-shoulda
|
48
|
+
prerelease: false
|
49
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
50
|
+
none: false
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
segments:
|
55
|
+
- 0
|
56
|
+
version: "0"
|
57
|
+
type: :development
|
58
|
+
version_requirements: *id003
|
59
|
+
description: QStat Ruby Frontend (Real-time game server stat fetcher)
|
60
|
+
email: sub+peerler@gmail.com
|
61
|
+
executables: []
|
62
|
+
|
63
|
+
extensions: []
|
64
|
+
|
65
|
+
extra_rdoc_files:
|
66
|
+
- LICENSE
|
67
|
+
- README.rdoc
|
68
|
+
files:
|
69
|
+
- .document
|
70
|
+
- .gitignore
|
71
|
+
- LICENSE
|
72
|
+
- README.rdoc
|
73
|
+
- Rakefile
|
74
|
+
- VERSION
|
75
|
+
- lib/ruby-qstat.rb
|
76
|
+
- test/ruby-qstat_test.rb
|
77
|
+
- test/test_helper.rb
|
78
|
+
has_rdoc: true
|
79
|
+
homepage: http://github.com/kimoto/ruby-qstat
|
80
|
+
licenses: []
|
81
|
+
|
82
|
+
post_install_message:
|
83
|
+
rdoc_options:
|
84
|
+
- --charset=UTF-8
|
85
|
+
require_paths:
|
86
|
+
- lib
|
87
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
88
|
+
none: false
|
89
|
+
requirements:
|
90
|
+
- - ">="
|
91
|
+
- !ruby/object:Gem::Version
|
92
|
+
segments:
|
93
|
+
- 0
|
94
|
+
version: "0"
|
95
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
96
|
+
none: false
|
97
|
+
requirements:
|
98
|
+
- - ">="
|
99
|
+
- !ruby/object:Gem::Version
|
100
|
+
segments:
|
101
|
+
- 0
|
102
|
+
version: "0"
|
103
|
+
requirements: []
|
104
|
+
|
105
|
+
rubyforge_project:
|
106
|
+
rubygems_version: 1.3.7
|
107
|
+
signing_key:
|
108
|
+
specification_version: 3
|
109
|
+
summary: QStat Ruby Frontend (Real-time game server stat fetcher)
|
110
|
+
test_files:
|
111
|
+
- test/test_helper.rb
|
112
|
+
- test/ruby-qstat_test.rb
|