1
2
3
4
5
6 require 'time'
7 require 'csv'
8
9 Stats = Struct.new(:min,:max,:n,:sx,:sx2)
10
11
12 class Stats
13 def initialize(population)
14 self.n = 0
15 self.sx = self.sx2 = 0.0
16 population.each { |x|
17 self.n += 1
18 self.sx += x
19 self.sx2 += x*x;
20 self.min = x if self.min.nil? or x < self.min
21 self.max = x if self.max.nil? or x > self.max
22 }
23 end
24
25 def mean
26 sx/n
27 end
28
29 def variance
30 sx2/n - (sx/n)**2
31 end
32
33 def stdev
34 variance ** 0.5
35 end
36
37 def to_s
38 "#{mean} (dev: #{stdev}, range: #{min} - #{max}, n: #{n})"
39 end
40 end
41
42 module Procalyzer
43
44 LogEntry = Struct.new(:time,:message,:actor_guid,:actor_name,:actor_id,:target_guid,:target_name,:target_id,:rest)
45
46
47
48
49 def self.parse_log(stream)
50 lines = []
51 while line=stream.gets
52 entry = LogEntry.new
53 line =~ %r{^(\d+)/(\d+) (\d\d):(\d\d):(\d\d).(\d\d\d) (.*)$} or raise "Couldnt parse line: #{line}"
54 entry.time = Time.local(Time.new.year, $1.to_i, $2.to_i, $3.to_i, $4.to_i, $5.to_i, 1000*$6.to_i)
55 entry.message,
56 entry.actor_guid, entry.actor_name, entry.actor_id,
57 entry.target_guid, entry.target_name, entry.target_id,
58 *rest = *CSV.parse_line($7.strip)
59 entry.rest = rest
60 if block_given?
61 lines << yield(entry)
62 else
63 lines << entry
64 end
65 end
66 lines
67 end
68
69
70 class EventScanner
71 def initialize
72 @filters = []
73 @start = []
74 @end = []
75 end
76
77
78 def on(filter, &block)
79 @filters << [filter,block]
80 end
81
82 def start(&block)
83 @start << block
84 end
85
86 def end(&block)
87 @end << block
88 end
89
90
91 def run(file)
92 @start.each {|s| s.call() }
93 Procalyzer::parse_log(file) {|entry|
94 @filters.each {|filter,block|
95 block.call(entry) if filter[entry]
96 }
97 }
98 @end.each {|s| s.call() }
99 end
100 end
101
102
103
104
105
106
107 ProcResults = Struct.new(:durations, :intervals, :cooldown, :procs)
108 class ProcResults
109 def initialize
110 self.durations=[]
111 self.intervals=[]
112 self.procs=[]
113 end
114
115 def dump(out=$stdout)
116 if self.durations.empty?
117 out.puts "Proc did not occur."
118 elsif self.intervals.empty?
119 out.puts "Proc occurred once only."
120 else
121 dur_stats = Stats.new(durations)
122 out.puts "Duration: #{dur_stats}"
123 if dur_stats.stdev/dur_stats.mean > 0.1
124 out.puts "Standard deviation of duration is more than 10%."
125 out.puts "This can indicate a proc with no cooldown that refreshes itself."
126 end
127 interval_stats = Stats.new(intervals)
128 out.puts "Interval: #{interval_stats}"
129 out.printf("Uptime: %.2f%%\n", 100.0*(dur_stats.sx - durations[-1])/interval_stats.sx)
130 out.puts "Internal cooldown: #{cooldown}"
131 proc_stats = Stats.new(procs)
132 out.printf("Proc chance: %.2f%% (%d/%d)\n", 100*proc_stats.mean, proc_stats.sx.to_i, proc_stats.n)
133 end
134 end
135 end
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150 def self.procalyze(file, auraname, cooldown=nil, &proc_trigger)
151 results = ProcResults.new
152 results.cooldown=cooldown
153
154 scanner = EventScanner.new
155
156 start = nil
157 last = nil
158
159
160 scanner.on(proc {|e| e.message == "SPELL_AURA_APPLIED" and e.rest[1] == auraname} ) {|evt|
161 start = evt.time.to_f
162 results.intervals << (start-last) if last
163 last = start
164 }
165
166 scanner.on(proc {|e| e.message == "SPELL_AURA_REMOVED" and e.rest[1] == auraname} ) {|evt|
167 if start
168 duration = evt.time.to_f - start
169 results.durations << duration
170 end
171 }
172 scanner.run(file)
173
174 return results if results.intervals.length==0
175
176
177 if results.cooldown.nil?
178 interval_stats = Stats.new(results.intervals)
179 duration_stats = Stats.new(results.durations)
180 if(duration_stats.stdev/duration_stats.mean > 0.1)
181 results.cooldown = 0.0
182 else
183 results.cooldown = interval_stats.min.floor
184 end
185 puts "Using cooldown = #{results.cooldown}" if $DEBUG
186 end
187
188 file.pos = 0
189 last_swing_time = nil
190 last_swing_could_proc = false
191 aura_up = false
192 start = nil
193 scanner = EventScanner.new
194
195 start_swing = proc{|evt| last_swing_time = evt.time.to_f; last_swing_count_proc = true }
196 end_swing = proc{
197 if(last_swing_could_proc)
198 puts "Possible proc: #{last_swing_time}" if $DEBUG
199 results.procs << (aura_up ? 1 : 0)
200 end
201 last_swing_could_proc = false
202 last_swing_time = nil
203 }
204
205
206 scanner.on(proc {|e| e.message == "SPELL_AURA_APPLIED" and e.rest[1] == auraname} ) {|evt|
207 puts "Aura start: #{evt.time.to_f}" if $DEBUG
208 start = evt.time.to_f
209 aura_up = true
210 end_swing.call()
211 }
212
213 scanner.on(proc {|e| e.message == "SPELL_AURA_REMOVED" and e.rest[1] == auraname} ) {|evt|
214 aura_up = false
215 }
216
217 scanner.on(proc_trigger) {|evt|
218
219 end_swing.call()
220
221
222 time_since_last = (start.nil?) ? 99999.0 : (evt.time.to_f - start)
223 last_swing_could_proc = (not aura_up) && (time_since_last > results.cooldown)
224 last_swing_time = evt.time.to_f
225 }
226
227 scanner.end(&end_swing)
228
229 scanner.run(file)
230 results
231 end
232
233
234 PLAYER_ID = "0x511"
235
236
237 PLAYER_SWING = proc{|evt| evt.actor_id == PLAYER_ID and evt.message="SWING_DAMAGE" }
238 PLAYER_SWING_OR_SPELL = proc{|evt| evt.actor_id == PLAYER_ID and (evt.message=="SWING_DAMAGE" or evt.message=="SPELL_DAMAGE") }
239 PLAYER_SPELL_DAMAGE = proc{|evt| evt.actor_id == PLAYER_ID and evt.message=="SPELL_DAMAGE" }
240 PLAYER_RANGED_OR_SPELL = proc{|evt| evt.actor_id == PLAYER_ID and (evt.message=="RANGE_DAMAGE" or evt.message=="SPELL_DAMAGE") }
241 end