tclog 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.mkd +40 -0
- data/Rakefile +13 -0
- data/VERSION +1 -0
- data/lib/tclog.rb +289 -0
- data/misc/bctest.log +713 -0
- data/misc/bctest2.log +162 -0
- data/misc/ctftest.log +166 -0
- data/misc/objtest.log +593 -0
- data/misc/test.log +189 -0
- data/spec/tclog_spec.rb +193 -0
- data/tclog.gemspec +50 -0
- metadata +74 -0
data/README.mkd
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
# TCLog: parser for etconsole.log of TrueCombat:Elite (TC:E)
|
2
|
+
|
3
|
+
## Install
|
4
|
+
|
5
|
+
$ gem install tclog
|
6
|
+
|
7
|
+
Coming soon!
|
8
|
+
|
9
|
+
## Usage
|
10
|
+
|
11
|
+
require 'tclog'
|
12
|
+
|
13
|
+
g = TCLog.analyze("/path/to/etconsole.log")
|
14
|
+
|
15
|
+
p g.players
|
16
|
+
p g.rounds[0]
|
17
|
+
|
18
|
+
## License
|
19
|
+
|
20
|
+
The MIT License
|
21
|
+
|
22
|
+
(c) Shota Fukumori (sora_h) 2010-
|
23
|
+
|
24
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
25
|
+
of this software and associated documentation files (the "Software"), to deal
|
26
|
+
in the Software without restriction, including without limitation the rights
|
27
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
28
|
+
copies of the Software, and to permit persons to whom the Software is
|
29
|
+
furnished to do so, subject to the following conditions:
|
30
|
+
|
31
|
+
The above copyright notice and this permission notice shall be included in
|
32
|
+
all copies or substantial portions of the Software.
|
33
|
+
|
34
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
35
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
36
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
37
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
38
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
39
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
40
|
+
THE SOFTWARE.
|
data/Rakefile
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
begin
|
2
|
+
require 'jeweler'
|
3
|
+
Jeweler::Tasks.new do |gemspec|
|
4
|
+
gemspec.name = "tclog"
|
5
|
+
gemspec.summary = "Parser for etconsole.log of TrueCombat:Elite (TC:E)"
|
6
|
+
gemspec.email = "sorah@tubusu.net"
|
7
|
+
gemspec.homepage = "http://github.com/sorah/tclog"
|
8
|
+
gemspec.description = "Parser for etconsole.log of TrueCombat:Elite (TC:E)"
|
9
|
+
gemspec.authors = ["Shota Fukumori"]
|
10
|
+
end
|
11
|
+
rescue LoadError
|
12
|
+
puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
|
13
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.1.0
|
data/lib/tclog.rb
ADDED
@@ -0,0 +1,289 @@
|
|
1
|
+
module TCLog
|
2
|
+
class Game
|
3
|
+
def initialize(orders = [], gametype = :obj)
|
4
|
+
@orders = orders
|
5
|
+
@gametype = gametype
|
6
|
+
@rounds = []
|
7
|
+
@round_n = -1
|
8
|
+
@round_r = -1
|
9
|
+
@players = {}
|
10
|
+
class << @players
|
11
|
+
def [](x)
|
12
|
+
if x.kind_of?(Integer)
|
13
|
+
self.map{|v|v}[x]
|
14
|
+
else
|
15
|
+
super x
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def each
|
20
|
+
super &(Proc.new{|k,v| yield v })
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
|
26
|
+
def add_map(map_name)
|
27
|
+
@round_r += 1
|
28
|
+
@rounds << Round.new(self, nil, @round_r, :map, true, map_name); self
|
29
|
+
end
|
30
|
+
|
31
|
+
def add_round(specops,terrorists,won)
|
32
|
+
@round_n += 1
|
33
|
+
@round_r += 1
|
34
|
+
r = Round.new(self, @round_n, @round_r, won)
|
35
|
+
r.specops = specops.dup
|
36
|
+
r.terrorists = terrorists.dup
|
37
|
+
@rounds << r
|
38
|
+
self
|
39
|
+
end
|
40
|
+
|
41
|
+
def add_player(name)
|
42
|
+
@players[name] = Player.new(name)
|
43
|
+
if 0 <= @round_r
|
44
|
+
@players[name].push_result(@round_r)
|
45
|
+
end
|
46
|
+
self
|
47
|
+
end
|
48
|
+
|
49
|
+
|
50
|
+
def round; @round_n; end
|
51
|
+
def [](i); @rounds[i]; end
|
52
|
+
attr_reader :rounds, :players, :orders, :gametype, :round_r
|
53
|
+
end
|
54
|
+
class Round
|
55
|
+
def initialize(game, n, rn, win, map_changing = false, map_name = nil)
|
56
|
+
@game = game
|
57
|
+
@won = win
|
58
|
+
@round_number = n
|
59
|
+
@real_round_number = rn
|
60
|
+
@map_changing = map_changing
|
61
|
+
@specops = {}
|
62
|
+
@terrorists = {}
|
63
|
+
@map_name = map_name
|
64
|
+
end
|
65
|
+
|
66
|
+
def players
|
67
|
+
@game.players.map do |g|
|
68
|
+
g if @round_number && g.results[@real_round_number]
|
69
|
+
end.compact
|
70
|
+
end
|
71
|
+
|
72
|
+
def player_results
|
73
|
+
@game.players.map do |g|
|
74
|
+
g.results[@real_round_number] if @round_number
|
75
|
+
end.compact
|
76
|
+
end
|
77
|
+
|
78
|
+
def map_changing?; @map_changing; end
|
79
|
+
def map_changing=(x); @map_changing = x; end
|
80
|
+
attr_accessor :map_name, :specops, :terrorists, :won
|
81
|
+
attr_reader :round_number
|
82
|
+
end
|
83
|
+
class Player
|
84
|
+
def initialize(name)
|
85
|
+
@name = name
|
86
|
+
@results = []
|
87
|
+
end
|
88
|
+
|
89
|
+
def add_result(n, score)
|
90
|
+
@results[n] = score.merge(:round => n); self
|
91
|
+
end
|
92
|
+
|
93
|
+
def total
|
94
|
+
a = @results.compact.inject({
|
95
|
+
:name => @name,
|
96
|
+
:kill => 0,
|
97
|
+
:death => 0,
|
98
|
+
:sui => 0,
|
99
|
+
:tk => 0,
|
100
|
+
:eff => 0,
|
101
|
+
:dg => 0,
|
102
|
+
:dr => 0,
|
103
|
+
:td => 0,
|
104
|
+
:score => 0,
|
105
|
+
:rate => 0
|
106
|
+
}) do |r,i|
|
107
|
+
[:kill,:death,:sui,:tk,:eff,:dg,:dr,:td,:score].each do |l|
|
108
|
+
r[l] += i[l]
|
109
|
+
end
|
110
|
+
r
|
111
|
+
end
|
112
|
+
a[:rate] = (a[:dg].to_i-a[:dr].to_i)/100.0
|
113
|
+
a[:kd] = a[:kill].to_f / a[:death].to_f
|
114
|
+
a
|
115
|
+
end
|
116
|
+
|
117
|
+
def push_result(i)
|
118
|
+
@results[i] = nil
|
119
|
+
end
|
120
|
+
attr_reader :name, :results
|
121
|
+
end
|
122
|
+
|
123
|
+
# logfile = String: filename
|
124
|
+
# IO: io
|
125
|
+
#
|
126
|
+
# gametype = :obj => "Objective"
|
127
|
+
# :ctf => "Capture The Flag"
|
128
|
+
# :bc => "BodyCount"
|
129
|
+
def self.analyze(logfile, gametype = :obj)
|
130
|
+
# Load file
|
131
|
+
log = case logfile
|
132
|
+
when File, IO
|
133
|
+
logfile.readlines.map(&:chomp)
|
134
|
+
when String
|
135
|
+
File.readlines(logfile).map(&:chomp)
|
136
|
+
else
|
137
|
+
raise ArgumentError, "logfile must be kind of String, File or IO"
|
138
|
+
end
|
139
|
+
# Drop waste lines / Drop waste message
|
140
|
+
renames = {}
|
141
|
+
orders = log.map do |x|
|
142
|
+
next unless /^(\[skipnotify\])(\^.)?(.+renamed|Timelimit hit)/ =~ x ||
|
143
|
+
/^(\[skipnotify\])?(\^.)?(Specops|Terrorists)/ =~ x ||
|
144
|
+
/^(\[skipnotify\])?(\^.)?(Planted|Defused)/ =~ x ||
|
145
|
+
/^The .+ have completed the objective!/ =~ x ||
|
146
|
+
/^(\[skipnotify\])(\^.)(Overall stats for: |)/ =~ x ||
|
147
|
+
/^(LOADING\.\.\. maps|Match starting)/ =~ x
|
148
|
+
x.gsub!(/\[skipnotify\]/,"")
|
149
|
+
# Player Score
|
150
|
+
r = x.scan(/\^.(Terrorists|Specops)\^. *\^.(.+?)\^. *([\d\-]+) +([\d\-]+) +([\d\-]+) +([\d\-]+)\^. *([\d\-]+)\^. *([\d\-]+)\^. *([\d\-]+)\^. *([\d\-]+)\^. *([\d\-]+)\^. *([\d\-]+)/).flatten
|
151
|
+
if r.empty? # If the line not player's score
|
152
|
+
case x
|
153
|
+
when /^LOADING\.\.\. maps\/(.+?)\.bsp$/ # Map Changing?
|
154
|
+
m = $~.captures
|
155
|
+
["Map",m[0]]
|
156
|
+
when /^Match starting/ # Match starting
|
157
|
+
["Match"]
|
158
|
+
when /^(Planted) at (.+?) \[(.)\]/, /^The Terrorists have completed the objective!/ # TeroWin
|
159
|
+
m = $~.captures
|
160
|
+
m[0] = "TerroristsWin"
|
161
|
+
m
|
162
|
+
when /^(Defused) at (.+?) \[(.)\]/, /^The Specops have completed the objective!/ # Specops Win
|
163
|
+
m = $~.captures
|
164
|
+
m[0] = "SpecopsWin"
|
165
|
+
m
|
166
|
+
when /Timelimit hit/
|
167
|
+
if gametype == :obj
|
168
|
+
["SpecopsWin"]
|
169
|
+
else
|
170
|
+
["UnknownWin"]
|
171
|
+
end
|
172
|
+
when /^\^7Overall stats for:/ # /^(\[skipnotify\])(\^.)(Overall stats for: |)/
|
173
|
+
if [:bc, :obj].include?(gametype)
|
174
|
+
["UnknownWin"]
|
175
|
+
else
|
176
|
+
nil
|
177
|
+
end
|
178
|
+
when /\^.(.+?)\^. \^.(Totals) *([\d\-]+) +([\d\-]+) +([\d\-]+) +([\d\-]+)\^. *([\d\-]+)\^. *([\d\-]+)\^. *([\d\-]+)\^. *([\d\-]+)\^. *([\d\-]+)\^. *([\d\-]+)/ # Team total score?
|
179
|
+
r = $~.captures
|
180
|
+
r[0] << "Total"
|
181
|
+
m = [r[0]]
|
182
|
+
m << {
|
183
|
+
:name => r[1],
|
184
|
+
:kill => r[2].to_i,
|
185
|
+
:death => r[3].to_i,
|
186
|
+
:sui => r[4].to_i,
|
187
|
+
:tk => r[5].to_i,
|
188
|
+
:eff => r[6].to_i,
|
189
|
+
:aa => r[7].to_i,
|
190
|
+
:dg => r[8].to_i,
|
191
|
+
:dr => r[9].to_i,
|
192
|
+
:td => r[10].to_i,
|
193
|
+
:score => r[11].to_i,
|
194
|
+
:rate => (r[8].to_i-r[9].to_i)/100.0,
|
195
|
+
}
|
196
|
+
when /^(.+) renamed to (.+)/ # nick renamed?
|
197
|
+
m = $~.captures
|
198
|
+
m.map!{|n|n.gsub(/ +$/,"")[0..14]}
|
199
|
+
renames[m[1]] = renames[m[0]] || m[0]
|
200
|
+
nil
|
201
|
+
end
|
202
|
+
else
|
203
|
+
# Process player's score
|
204
|
+
if r[1].kind_of?(String)
|
205
|
+
r[1].gsub!(/ +$/,"")
|
206
|
+
r[1] = r[1][0..14]
|
207
|
+
r[1] = renames[r[1]] || r[1]
|
208
|
+
end
|
209
|
+
m = [r[0]]
|
210
|
+
m << {
|
211
|
+
:name => r[1],
|
212
|
+
:kill => r[2].to_i,
|
213
|
+
:death => r[3].to_i,
|
214
|
+
:sui => r[4].to_i,
|
215
|
+
:tk => r[5].to_i,
|
216
|
+
:eff => r[6].to_i,
|
217
|
+
:aa => r[7].to_i,
|
218
|
+
:dg => r[8].to_i,
|
219
|
+
:dr => r[9].to_i,
|
220
|
+
:td => r[10].to_i,
|
221
|
+
:score => r[11].to_i,
|
222
|
+
:rate => (r[8].to_i-r[9].to_i)/100.0,
|
223
|
+
}
|
224
|
+
end
|
225
|
+
end.compact
|
226
|
+
|
227
|
+
#pp orders
|
228
|
+
|
229
|
+
match_flag = false
|
230
|
+
game = Game.new(orders, gametype)
|
231
|
+
match = [""]
|
232
|
+
match_wins = nil
|
233
|
+
terrorists_total = nil
|
234
|
+
specops_total = nil
|
235
|
+
vm = Proc.new do |o|
|
236
|
+
case o[0]
|
237
|
+
when "Match"
|
238
|
+
if match_flag
|
239
|
+
if specops_total && match_wins
|
240
|
+
game.add_round specops_total, terrorists_total, match_wins
|
241
|
+
match_wins = nil
|
242
|
+
terrorists_total = nil
|
243
|
+
specops_total = nil
|
244
|
+
end
|
245
|
+
else
|
246
|
+
match_flag = true
|
247
|
+
end
|
248
|
+
if match[-1][0] == "Map"
|
249
|
+
game.add_map match[-1][1]
|
250
|
+
end
|
251
|
+
when "UnknownWin"
|
252
|
+
match_wins = :unknown if match_flag
|
253
|
+
when "TerroristsWin"
|
254
|
+
match_wins = :terrorists if match_flag
|
255
|
+
when "SpecopsWin"
|
256
|
+
match_wins = :specops if match_flag
|
257
|
+
when "Terrorists", "Specops"
|
258
|
+
if match_flag
|
259
|
+
unless game.players[o[1][:name]]
|
260
|
+
game.add_player(o[1][:name])
|
261
|
+
end
|
262
|
+
game.players[o[1][:name]].add_result(game.round_r+1,o[1])
|
263
|
+
end
|
264
|
+
when "TerroristsTotal"
|
265
|
+
terrorists_total = o[1] if match_flag
|
266
|
+
when "SpecopsTotal"
|
267
|
+
if match_flag
|
268
|
+
specops_total = o[1]
|
269
|
+
match_wins = compare_score(terrorists_total, specops_total) if match_wins == :unknown
|
270
|
+
end
|
271
|
+
end
|
272
|
+
match << o
|
273
|
+
end
|
274
|
+
orders.each(&vm)
|
275
|
+
|
276
|
+
vm.call(["Match"]) if match_flag
|
277
|
+
game
|
278
|
+
end
|
279
|
+
|
280
|
+
def self.compare_score(terrorists, specops)
|
281
|
+
if terrorists[:score] > specops[:score]
|
282
|
+
:terrorists
|
283
|
+
else
|
284
|
+
:specops
|
285
|
+
end
|
286
|
+
end
|
287
|
+
end
|
288
|
+
|
289
|
+
|