ruby-qstat 0.1.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/.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
|