Never been to DZone Snippets before?

Snippets is a public source code repository. Easily build up your personal collection of code snippets, categorize them with tags / keywords, and share them with the world

Procalyzer for non-personal logs (See related posts)

Tunah's Procalyzer script, slightly updated to work with Hunter ranged attacks, and to deal with multiple people in a log having procs with the same name. This version also displays an alternate uptime calculation that does not discard the last proc duration.

   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.


Click here to browse all 5545 code snippets

Related Posts