1 2 # Original procalyzer script written by Tunah: http://snippets.dzone.com/posts/show/5322 3 # 1.1 modified to work with ranged attacks 4 # 1.2 modified to only count the particular player (PLAYER_ID)'s procs 5 require 'time' 6 require 'csv' 7 8 Stats = Struct.new(:min,:max,:n,:sx,:sx2) 9 10 # actor_id for the player 11 PLAYER_ID = "0x511" 12 PLAYER_NAME = "Fafhrd" 13 14 # calculate mean, stdev, etc for an array of numbers 15 class Stats 16 def initialize(population) 17 self.n = 0 18 self.sx = self.sx2 = 0.0 19 population.each { |x| 20 self.n += 1 21 self.sx += x 22 self.sx2 += x*x; 23 self.min = x if self.min.nil? or x < self.min 24 self.max = x if self.max.nil? or x > self.max 25 } 26 end 27 28 def mean 29 sx/n 30 end 31 32 def variance 33 sx2/n - (sx/n)**2 34 end 35 36 def stdev 37 variance ** 0.5 38 end 39 40 def to_s 41 "#{mean} (dev: #{stdev}, range: #{min} - #{max}, n: #{n}, total: #{sx})" 42 end 43 end 44 45 module Procalyzer 46 # wow 2.4 log entry 47 LogEntry = Struct.new(:time,:message,:actor_guid,:actor_name,:actor_id,:target_guid,:target_name,:target_id,:rest) 48 49 # parse a log into a list of LogEntries. 50 # if block given, yields each entry and returns a list of the result 51 # else resturns a list of entries 52 def self.parse_log(stream) 53 lines = [] 54 while line=stream.gets 55 entry = LogEntry.new 56 line =~ %r{^(\d+)/(\d+) (\d\d):(\d\d):(\d\d).(\d\d\d) (.*)$} or raise "Couldnt parse line: #{line}" 57 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) 58 entry.message, 59 entry.actor_guid, entry.actor_name, entry.actor_id, 60 entry.target_guid, entry.target_name, entry.target_id, 61 *rest = *CSV.parse_line($7.strip) 62 entry.rest = rest 63 if block_given? 64 lines << yield(entry) 65 else 66 lines << entry 67 end 68 end 69 lines 70 end 71 72 # Scans a logfile, runs event handlers for events matching certain filters 73 class EventScanner 74 def initialize 75 @filters = [] 76 @start = [] 77 @end = [] 78 end 79 80 # add a handler to be run for events matching filter 81 def on(filter, &block) 82 @filters << [filter,block] 83 end 84 # add a handler at the start 85 def start(&block) 86 @start << block 87 end 88 # add a handler at the end 89 def end(&block) 90 @end << block 91 end 92 93 # scan the stream and run handlers 94 def run(file) 95 @start.each {|s| s.call() } 96 Procalyzer::parse_log(file) {|entry| 97 @filters.each {|filter,block| 98 block.call(entry) if filter[entry] 99 } 100 } 101 @end.each {|s| s.call() } 102 end 103 end 104 105 # Container for analysis results 106 # durations is a list of the proc lengths found 107 # intervals is the list of times from one proc to the next 108 # cooldown is the estimated or specified internal cooldown 109 # procs is a list of 1 or 0 entries specifying whether each eligible event procced the aura 110 ProcResults = Struct.new(:durations, :intervals, :cooldown, :procs) 111 class ProcResults 112 def initialize 113 self.durations=[] 114 self.intervals=[] 115 self.procs=[] 116 end 117 # print results in a pretty human readable form 118 def dump(out=$stdout) 119 if self.durations.empty? 120 out.puts "Proc did not occur." 121 elsif self.intervals.empty? 122 out.puts "Proc occurred once only." 123 else 124 dur_stats = Stats.new(durations) 125 out.puts "Duration: #{dur_stats}" 126 if dur_stats.stdev/dur_stats.mean > 0.1 127 out.puts "Standard deviation of duration is more than 10%." 128 out.puts "This can indicate a proc with no cooldown that refreshes itself." 129 end 130 interval_stats = Stats.new(intervals) 131 out.puts "Interval: #{interval_stats}" 132 out.printf("Uptime: %.2f%% (%.1fs/%.1fs)\n", 100.0*(dur_stats.sx - durations[-1])/interval_stats.sx,(dur_stats.sx - durations[-1]),interval_stats.sx) 133 out.printf("Uptime2: %.2f%% (%.1fs/%.1fs)\n", 100.0*(dur_stats.sx)/interval_stats.sx,(dur_stats.sx),interval_stats.sx) 134 out.puts "Internal cooldown: #{cooldown}" 135 proc_stats = Stats.new(procs) 136 out.printf("Proc chance: %.2f%% (%d/%d)\n", 100*proc_stats.mean, proc_stats.sx.to_i, proc_stats.n) 137 end 138 end 139 end 140 141 # Analyse a log file, looking for a self-buff triggered by an event. 142 # Assume it has a fixed % to proc unless it is on cooldown (or already active, 143 # in which case it could theoretically proc but won't slow in the log) 144 # we do this in two phases: 145 # 1) measure proc durations and intervals, and estimate cooldown if it's not specified 146 # (just minimum interval rounded down to nearest second) 147 # 2) count eligible proccing events (ones outside cooldown) and see which procced it 148 # file is a seekable stream containing the log 149 # auraname is the buff name like "Haste" 150 # cooldown is the internal cooldown if known (else will be estimated) 151 # proc_trigger is a filter block for proc events (takes an event, returns true if the event can proc 152 # the aura, assuming the aura isnt on cooldown or already up) 153 # returns a procresults object 154 def self.procalyze(file, auraname, cooldown=nil, &proc_trigger) 155 puts "Parsing for proc named #{auraname}" 156 results = ProcResults.new 157 results.cooldown=cooldown 158 159 scanner = EventScanner.new 160 161 start = nil 162 last = nil 163 164 # Aura start: record start time and interval 165 scanner.on(proc {|e| e.target_id == PLAYER_ID and e.message == "SPELL_AURA_APPLIED" and e.rest[1] == auraname} ) {|evt| 166 start = evt.time.to_f 167 results.intervals << (start-last) if last 168 last = start 169 } 170 # Aura finish: record duration 171 scanner.on(proc {|e| e.target_id == PLAYER_ID and e.message == "SPELL_AURA_REMOVED" and e.rest[1] == auraname} ) {|evt| 172 if start 173 duration = evt.time.to_f - start 174 results.durations << duration 175 end 176 } 177 scanner.run(file) 178 179 return results if results.intervals.length==0 180 181 # Estimate cooldown if not given 182 if results.cooldown.nil? 183 interval_stats = Stats.new(results.intervals) 184 duration_stats = Stats.new(results.durations) 185 if(duration_stats.stdev/duration_stats.mean > 0.1) 186 results.cooldown = 0.0 187 else 188 results.cooldown = interval_stats.min.floor 189 end 190 puts "Using cooldown = #{results.cooldown}" if $DEBUG 191 end 192 193 file.pos = 0 # restart file 194 last_swing_time = nil 195 last_swing_could_proc = false 196 aura_up = false 197 start = nil 198 scanner = EventScanner.new 199 200 start_swing = proc{|evt| last_swing_time = evt.time.to_f; last_swing_count_proc = true } 201 end_swing = proc{ 202 if(last_swing_could_proc) 203 puts "Possible proc: #{last_swing_time}" if $DEBUG 204 results.procs << (aura_up ? 1 : 0) 205 end 206 last_swing_could_proc = false 207 last_swing_time = nil 208 } 209 210 # Aura start, start cooldown timer and set aura_up 211 scanner.on(proc {|e| e.target_id == PLAYER_ID and e.message == "SPELL_AURA_APPLIED" and e.rest[1] == auraname} ) {|evt| 212 puts "Aura start: #{evt.time.to_f}" if $DEBUG 213 start = evt.time.to_f 214 aura_up = true 215 end_swing.call() 216 } 217 # Aura end, clear aura_up 218 scanner.on(proc {|e| e.target_id == PLAYER_ID and e.message == "SPELL_AURA_REMOVED" and e.rest[1] == auraname} ) {|evt| 219 aura_up = false 220 } 221 # Proc trigger event - eg a melee swing 222 scanner.on(proc_trigger) {|evt| 223 # first, this is the point where we confirm the previous swing didnt proc the aura 224 end_swing.call() 225 226 # record details of this swing, incl whether it could proc 227 time_since_last = (start.nil?) ? 99999.0 : (evt.time.to_f - start) 228 last_swing_could_proc = (not aura_up) && (time_since_last > results.cooldown) 229 last_swing_time = evt.time.to_f 230 } 231 # at the end of the log file, confirm that the last swing didnt proc the aura 232 scanner.end(&end_swing) 233 234 scanner.run(file) 235 results 236 end 237 238 # predefined filters for common procs 239 PLAYER_SWING = proc{|evt| evt.actor_id == PLAYER_ID and evt.message="SWING_DAMAGE" } 240 PLAYER_SWING_OR_SPELL = proc{|evt| evt.actor_id == PLAYER_ID and (evt.message=="SWING_DAMAGE" or evt.message=="SPELL_DAMAGE") } 241 PLAYER_SPELL_DAMAGE = proc{|evt| evt.actor_id == PLAYER_ID and evt.message=="SPELL_DAMAGE" } 242 PLAYER_RANGED_DAMAGE_AND_SPELL = proc{|evt| evt.actor_id == PLAYER_ID and (evt.message=="RANGE_DAMAGE" or evt.message=="SPELL_DAMAGE") } 243 PLAYER_RANGED_DAMAGE = proc{|evt| evt.actor_id == PLAYER_ID and (evt.message=="RANGE_DAMAGE") } 244 NAME_SWING_OR_SPELL = proc{|evt| evt.actor_name == PLAYER_NAME and (evt.message=="SWING_DAMAGE" or evt.message=="SPELL_DAMAGE") } 245 end 246 247 File.open('Nalorakk.txt') {|log| 248 Procalyzer::procalyze(log,"Forceful Strike",&Procalyzer::PLAYER_RANGED_DAMAGE_AND_SPELL).dump 249 }
You need to create an account or log in to post comments to this site.