From 93efc6051269348a955f79d34ae151560fbcb0d3 Mon Sep 17 00:00:00 2001 From: Daniel Schadt Date: Thu, 14 Jun 2018 09:20:50 +0200 Subject: rework boon tracking --- src/lib.rs | 1 + src/main.rs | 7 +++ src/statistics/boon.rs | 123 +++++++++++++++++++++++++++++++++++++++++++++ src/statistics/math.rs | 36 +++++++++++++ src/statistics/mod.rs | 22 ++++++-- src/statistics/trackers.rs | 99 +++++++++++++++++++----------------- 6 files changed, 238 insertions(+), 50 deletions(-) (limited to 'src') diff --git a/src/lib.rs b/src/lib.rs index e793760..5b3d661 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -22,6 +22,7 @@ extern crate quick_error; #[macro_use] extern crate num_derive; extern crate byteorder; +extern crate fnv; extern crate num_traits; pub mod raw; diff --git a/src/main.rs b/src/main.rs index 610ba40..673c396 100644 --- a/src/main.rs +++ b/src/main.rs @@ -148,5 +148,12 @@ pub fn main() -> Result<(), evtclib::raw::parser::ParseError> { println!("Damages: {:?}", stats.damage_log); println!("My damage: {:?}", my_damage); + for boon in evtclib::statistics::gamedata::BOONS { + let avg = mine + .boon_log + .average_stacks(mine.enter_combat, mine.exit_combat, boon.0); + println!("{}: {}", boon.1, avg); + } + Ok(()) } diff --git a/src/statistics/boon.rs b/src/statistics/boon.rs index 425f4a5..8175537 100644 --- a/src/statistics/boon.rs +++ b/src/statistics/boon.rs @@ -1,5 +1,14 @@ +//! Module providing functions and structs to deal with boon related statistics. use std::cmp; +use std::collections::HashMap; +use std::fmt; +use std::ops::Mul; + +use super::math::{Monoid, RecordFunc, Semigroup}; + +use fnv::FnvHashMap; + /// The type of a boon. #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum BoonType { @@ -40,6 +49,7 @@ pub struct BoonQueue { capacity: u32, queue: Vec, boon_type: BoonType, + next_update: u64, } impl BoonQueue { @@ -52,6 +62,7 @@ impl BoonQueue { capacity, queue: Vec::new(), boon_type, + next_update: 0, } } @@ -75,6 +86,7 @@ impl BoonQueue { pub fn add_stack(&mut self, duration: u64) { self.queue.push(duration); self.fix_queue(); + self.next_update = self.next_change(); } /// Return the amount of current stacks. @@ -100,6 +112,10 @@ impl BoonQueue { if duration == 0 { return; } + if duration < self.next_update { + self.next_update -= duration; + return; + } let mut remaining = duration; match self.boon_type { BoonType::Duration => { @@ -125,6 +141,7 @@ impl BoonQueue { .collect(); } } + self.next_update = self.next_change(); } /// Remove all stacks. @@ -163,6 +180,112 @@ impl BoonQueue { } } +/// Amount of stacks of a boon. +// Since this is also used to represent changes in stacks, we need access to +// negative numbers too, as stacks can drop. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Stacks(i32); + +impl Semigroup for Stacks { + #[inline] + fn combine(&self, other: &Self) -> Self { + Stacks(self.0 + other.0) + } +} + +impl Monoid for Stacks { + #[inline] + fn mempty() -> Self { + Stacks(0) + } +} + +// This shouldn't be negative, as total stacks are always greater than 0, thus +// the area below the curve will always be positive. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[doc(hidden)] +pub struct BoonArea(u64); + +impl Semigroup for BoonArea { + #[inline] + fn combine(&self, other: &Self) -> Self { + BoonArea(self.0 + other.0) + } +} + +impl Monoid for BoonArea { + #[inline] + fn mempty() -> Self { + BoonArea(0) + } +} + +impl Mul for Stacks { + type Output = BoonArea; + + #[inline] + fn mul(self, rhs: u64) -> BoonArea { + BoonArea(self.0 as u64 * rhs) + } +} + +/// A boon log for a specific player. +/// +/// This logs the amount of stacks of each boon a player had at any given time. +#[derive(Clone, Default)] +pub struct BoonLog { + // Keep a separate RecordFunc for each boon. + inner: FnvHashMap>, +} + +impl BoonLog { + /// Create a new, empty boon log. + pub fn new() -> Self { + Default::default() + } + + /// Add an event to the boon log. + pub fn log(&mut self, time: u64, boon_id: u16, stacks: u32) { + let func = self.inner.entry(boon_id).or_insert_with(Default::default); + let current = func.tally(); + if current.0 == stacks as i32 { + return; + } + let diff = stacks as i32 - current.0; + func.insert(time, (), Stacks(diff)); + } + + /// Get the average amount of stacks between the two given time points. + /// + /// * `a` - Start time point. + /// * `b` - End time point. + /// * `boon_id` - ID of the boon that you want to get the average for. + pub fn average_stacks(&self, a: u64, b: u64, boon_id: u16) -> f32 { + assert!(b > a); + let func = if let Some(f) = self.inner.get(&boon_id) { + f + } else { + return 0.; + }; + let area = func.integral(&a, &b); + area.0 as f32 / (b - a) as f32 + } + + /// Get the amount of stacks at the given time point. + /// + /// * `x` - Time point. + /// * `boon_id` - ID of the boon that you want to get. + pub fn stacks_at(&self, x: u64, boon_id: u16) -> u32 { + self.inner.get(&boon_id).map(|f| f.get(&x)).unwrap_or(0) + } +} + +impl fmt::Debug for BoonLog { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "BoonLog {{ .. }}") + } +} + #[cfg(test)] mod test { use super::*; diff --git a/src/statistics/math.rs b/src/statistics/math.rs index f0b0384..b7dd6ac 100644 --- a/src/statistics/math.rs +++ b/src/statistics/math.rs @@ -1,5 +1,8 @@ //! This module provides some basic mathematical structures. +use std::iter; +use std::ops::{Mul, Sub}; + /// A semigroup. /// /// This trait lets you combine elements by a binary operation. @@ -138,6 +141,39 @@ where } } +impl RecordFunc +where + X: Ord + Sub + Copy, + D: Monoid + Mul, + A: Monoid, +{ + #[inline] + pub fn integral(&self, a: &X, b: &X) -> A { + self.integral_only(a, b, |_| true) + } + + pub fn integral_only bool>(&self, a: &X, b: &X, mut predicate: F) -> A { + let points = self + .data + .iter() + .skip_while(|record| record.x < *a) + .take_while(|record| record.x <= *b) + .filter(|record| predicate(&record.tag)) + .map(|record| record.x) + .chain(iter::once(*b)) + .collect::>(); + let mut area = A::mempty(); + let mut last = *a; + for point in points { + let diff = point - last; + let value = self.get_only(&last, &mut predicate); + area = area.combine(&(value * diff)); + last = point; + } + area + } +} + impl Default for RecordFunc { fn default() -> Self { Self::new() diff --git a/src/statistics/mod.rs b/src/statistics/mod.rs index 5f2f288..0254135 100644 --- a/src/statistics/mod.rs +++ b/src/statistics/mod.rs @@ -9,6 +9,7 @@ pub mod gamedata; pub mod math; pub mod trackers; +use self::boon::BoonLog; use self::damage::DamageLog; use self::trackers::{RunnableTracker, Tracker}; @@ -53,7 +54,7 @@ pub struct AgentStats { /// /// For duration-based boons, the average amount of stacks is the same as /// the uptime. - pub boon_averages: HashMap, + pub boon_log: BoonLog, /// Time when the agent has entered combat (millseconds since log start). pub enter_combat: u64, /// Time when the agent has left combat (millseconds since log start). @@ -87,6 +88,7 @@ pub fn calculate(log: &Log) -> StatResult { let mut damage_tracker = trackers::DamageTracker::new(log); let mut log_start_tracker = trackers::LogStartTracker::new(); let mut combat_time_tracker = trackers::CombatTimeTracker::new(); + let mut boon_tracker = trackers::BoonTracker::new(); run_trackers( log, @@ -94,6 +96,7 @@ pub fn calculate(log: &Log) -> StatResult { &mut damage_tracker, &mut log_start_tracker, &mut combat_time_tracker, + &mut boon_tracker, ], )?; @@ -104,14 +107,27 @@ pub fn calculate(log: &Log) -> StatResult { let agent = agent_stats .entry(*agent_addr) .or_insert_with(Default::default); + // XXX: This used to be enter_time - log_start_time, as it makes more + // sense to have the time relative to the log start instead of the + // Windows boot time. However, this also means that we need to modify + // all event times before we do any tracking, as many trackers rely on + // event.time to track information related to time. if enter_time != 0 { - agent.enter_combat = enter_time - log_start_time; + agent.enter_combat = enter_time; } if exit_time != 0 { - agent.exit_combat = exit_time - log_start_time; + agent.exit_combat = exit_time; } } + let boon_logs = try_tracker!(boon_tracker.finalize()); + for (agent_addr, boon_log) in boon_logs { + let agent = agent_stats + .entry(agent_addr) + .or_insert_with(Default::default); + agent.boon_log = boon_log; + } + let damage_log = try_tracker!(damage_tracker.finalize()); Ok(Statistics { diff --git a/src/statistics/trackers.rs b/src/statistics/trackers.rs index 11e51f2..5eda86c 100644 --- a/src/statistics/trackers.rs +++ b/src/statistics/trackers.rs @@ -20,10 +20,12 @@ use std::collections::HashMap; use std::error::Error; use super::super::{Event, EventKind, Log}; -use super::boon::BoonQueue; +use super::boon::{BoonLog, BoonQueue}; use super::damage::{DamageLog, DamageType}; use super::gamedata::{self, Mechanic, Trigger}; +use fnv::FnvHashMap; + /// A tracker. /// /// A tracker should be responsible for tracking a single statistic. Each @@ -224,22 +226,18 @@ impl Tracker for CombatTimeTracker { /// This tracker only tracks the boons that are known to evtclib, that is the /// boons defined in `evtclib::statistics::gamedata::BOONS`. pub struct BoonTracker { - agent_addr: u64, - boon_areas: HashMap, - boon_queues: HashMap, + boon_logs: FnvHashMap, + boon_queues: FnvHashMap>, last_time: u64, - next_update: u64, } impl BoonTracker { /// Creates a new boon tracker for the given agent. - pub fn new(agent_addr: u64) -> BoonTracker { + pub fn new() -> BoonTracker { BoonTracker { - agent_addr, - boon_areas: Default::default(), + boon_logs: Default::default(), boon_queues: Default::default(), last_time: 0, - next_update: 0, } } @@ -247,46 +245,50 @@ impl BoonTracker { /// /// * `delta_t` - Amount of milliseconds to update. fn update_queues(&mut self, delta_t: u64) { + if delta_t == 0 { + return; + } + self.boon_queues .values_mut() + .flat_map(|m| m.values_mut()) .for_each(|queue| queue.simulate(delta_t)); // Throw away empty boon queues or to improve performance + self.boon_queues + .values_mut() + .for_each(|m| m.retain(|_, q| !q.is_empty())); self.boon_queues.retain(|_, q| !q.is_empty()); } - /// Update the internal tracking areas. - /// - /// Does not update the boon queues. - /// - /// * `delta_t` - Amount of milliseconds that passed. - fn update_areas(&mut self, delta_t: u64) { - for (buff_id, queue) in &self.boon_queues { - let current_stacks = queue.current_stacks(); - let area = self.boon_areas.entry(*buff_id).or_insert(0); - *area += current_stacks as u64 * delta_t; + fn update_logs(&mut self, time: u64) { + if time == self.last_time { + return; + } + for (agent, boons) in &self.boon_queues { + let agent_log = self + .boon_logs + .entry(*agent) + .or_insert_with(Default::default); + for (boon_id, queue) in boons { + agent_log.log(time, *boon_id, queue.current_stacks()); + } } - } - - fn update_next_update(&mut self) { - let next_update = self - .boon_queues - .values() - .map(BoonQueue::next_update) - .filter(|v| *v != 0) - .min() - .unwrap_or(0); - self.next_update = next_update; } /// Get the boon queue for the given buff_id. /// /// If the queue does not yet exist, create it. /// + /// * `agent_addr` - The address of the agent. /// * `buff_id` - The buff (or condition) id. - fn get_queue(&mut self, buff_id: u16) -> Option<&mut BoonQueue> { + fn get_queue(&mut self, agent_addr: u64, buff_id: u16) -> Option<&mut BoonQueue> { use std::collections::hash_map::Entry; - let entry = self.boon_queues.entry(buff_id); + let entry = self + .boon_queues + .entry(agent_addr) + .or_insert_with(Default::default) + .entry(buff_id); match entry { // Queue already exists Entry::Occupied(e) => Some(e.into_mut()), @@ -304,45 +306,48 @@ impl BoonTracker { } impl Tracker for BoonTracker { - type Stat = HashMap; + type Stat = HashMap; type Error = !; fn feed(&mut self, event: &Event) -> Result<(), Self::Error> { - let delta_t = event.time - self.last_time; - if self.next_update != 0 && delta_t > self.next_update { - self.update_queues(delta_t); - self.update_areas(delta_t); - self.update_next_update(); - self.last_time = event.time; - } + self.update_queues(delta_t); match event.kind { EventKind::BuffApplication { - buff_id, duration, .. + destination_agent_addr, + buff_id, + duration, + .. } => { - if let Some(queue) = self.get_queue(buff_id) { + if let Some(queue) = self.get_queue(destination_agent_addr, buff_id) { queue.add_stack(duration as u64); } - self.update_next_update(); } // XXX: Properly handle SINGLE and MANUAL removal types - EventKind::BuffRemove { buff_id, .. } => { - if let Some(queue) = self.get_queue(buff_id) { + EventKind::BuffRemove { + destination_agent_addr, + buff_id, + .. + } => { + if let Some(queue) = self.get_queue(destination_agent_addr, buff_id) { queue.clear(); } - self.update_next_update(); } _ => (), } + self.update_logs(event.time); + self.last_time = event.time; Ok(()) } fn finalize(self) -> Result { - Ok(self.boon_areas) + // Convert from FnvHashMap to HashMap in order to not leak + // implementation details. + Ok(self.boon_logs.into_iter().collect()) } } -- cgit v1.2.3