diff options
| -rw-r--r-- | src/gamedata.rs | 149 | ||||
| -rw-r--r-- | src/lib.rs | 5 | ||||
| -rw-r--r-- | src/statistics/boon.rs | 361 | ||||
| -rw-r--r-- | src/statistics/damage.rs | 95 | ||||
| -rw-r--r-- | src/statistics/gamedata.rs | 294 | ||||
| -rw-r--r-- | src/statistics/math.rs | 240 | ||||
| -rw-r--r-- | src/statistics/mechanics.rs | 62 | ||||
| -rw-r--r-- | src/statistics/mod.rs | 145 | ||||
| -rw-r--r-- | src/statistics/trackers.rs | 443 | 
9 files changed, 151 insertions, 1643 deletions
| diff --git a/src/gamedata.rs b/src/gamedata.rs new file mode 100644 index 0000000..c56af13 --- /dev/null +++ b/src/gamedata.rs @@ -0,0 +1,149 @@ +//! This module contains some low-level game data, such as different boss IDs. +use std::{fmt, str::FromStr}; +use num_derive::FromPrimitive; + +/// Enum containing all bosses with their IDs. +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, FromPrimitive)] +pub enum Boss { +    // Wing 1 +    ValeGuardian = 0x3C4E, +    Gorseval = 0x3C45, +    Sabetha = 0x3C0F, + +    // Wing 2 +    Slothasor = 0x3EFB, +    Matthias = 0x3EF3, + +    // Wing 3 +    KeepConstruct = 0x3F6B, +    /// Xera ID for phase 1. +    /// +    /// This is only half of Xera's ID, as there will be a second agent for the +    /// second phase. This agent will have another ID, see +    /// [`XERA_PHASE2_ID`](constant.XERA_PHASE2_ID.html). +    Xera = 0x3F76, + +    // Wing 4 +    Cairn = 0x432A, +    MursaatOverseer = 0x4314, +    Samarog = 0x4324, +    Deimos = 0x4302, + +    // Wing 5 +    SoullessHorror = 0x4D37, +    Dhuum = 0x4BFA, + +    // Wing 6 +    ConjuredAmalgamate = 0xABC6, +    LargosTwins = 0x5271, +    Qadim = 0x51C6, + +    // Wing 7 +    CardinalAdina = 0x55F6, +    CardinalSabir = 0x55CC, +    QadimThePeerless = 0x55F0, + +    // 100 CM +    Skorvald = 0x44E0, +    Artsariiv = 0x461D, +    Arkk = 0x455F, + +    // 99 CM +    MAMA = 0x427D, +    Siax = 0x4284, +    Ensolyss = 0x4234, + +    // Strike missions +    IcebroodConstruct = 0x568A, +    VoiceOfTheFallen = 0x5747, +    FraenirOfJormag = 0x57DC, +    Boneskinner = 0x57F9, +    WhisperOfJormag = 0x58B7, +} + + +/// Error for when converting a string to the boss fails. +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +pub struct ParseBossError(String); + + +impl fmt::Display for ParseBossError { +    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { +        write!(f, "Invalid boss identifier: {}", self.0) +    } +} + + +impl FromStr for Boss { +    type Err = ParseBossError; + +    fn from_str(s: &str) -> Result<Self, Self::Err> { +        let lower = s.to_lowercase(); +        match &lower as &str { +            "vg" | "vale guardian" => Ok(Boss::ValeGuardian), +            "gorse" | "gorseval" => Ok(Boss::Gorseval), +            "sab" | "sabetha" => Ok(Boss::Sabetha), + +            "sloth" | "slothasor" => Ok(Boss::Slothasor), +            "matthias" => Ok(Boss::Matthias), + +            "kc" | "keep construct" => Ok(Boss::KeepConstruct), +            "xera" => Ok(Boss::Xera), + +            "cairn" => Ok(Boss::Cairn), +            "mo" | "mursaat overseer" => Ok(Boss::MursaatOverseer), +            "sam" | "sama" | "samarog" => Ok(Boss::Samarog), +            "deimos" => Ok(Boss::Deimos), + +            "desmina" | "sh" => Ok(Boss::SoullessHorror), +            "dhuum" => Ok(Boss::Dhuum), + +            "ca" | "conjured almagamate" => Ok(Boss::ConjuredAmalgamate), +            "largos" | "twins" => Ok(Boss::LargosTwins), +            "qadim" => Ok(Boss::Qadim), + +            "adina" | "cardinal adina" => Ok(Boss::CardinalAdina), +            "sabir" | "cardinal sabir" => Ok(Boss::CardinalSabir), +            "qadimp" | "peerless qadim" | "qadim the peerless" => Ok(Boss::QadimThePeerless), + +            "skorvald" => Ok(Boss::Skorvald), +            "artsariiv" => Ok(Boss::Artsariiv), +            "arkk" => Ok(Boss::Arkk), + +            "mama" => Ok(Boss::MAMA), +            "siax" => Ok(Boss::Siax), +            "ensolyss" => Ok(Boss::Ensolyss), + +            "icebrood" | "icebrood construct" => Ok(Boss::IcebroodConstruct), +            "super kodan brothers" => Ok(Boss::VoiceOfTheFallen), +            "fraenir" | "fraenir of jormag" => Ok(Boss::FraenirOfJormag), +            "boneskinner" => Ok(Boss::Boneskinner), +            "whisper" | "whisper of jormag" => Ok(Boss::WhisperOfJormag), + +            _ => Err(ParseBossError(s.to_owned())) +        } +    } +} + + +/// ID for Xera in the second phase. +/// +/// The original Xera will despawn, and after the tower phase, a separate spawn +/// will take over. This new Xera will have this ID. Care needs to be taken when +/// calculating boss damage on this encounter, as both Xeras have to be taken +/// into account. +pub const XERA_PHASE2_ID: u16 = 0x3F9E; + + +#[cfg(test)] +mod tests { +    use super::*; + +    #[test] +    pub fn test_parsing() { +        assert_eq!("vg".parse(), Ok(Boss::ValeGuardian)); +        assert_eq!("VG".parse(), Ok(Boss::ValeGuardian)); + +        assert!("vga".parse::<Boss>().is_err()); +    } +} @@ -26,9 +26,8 @@ pub mod raw;  mod event;  pub use event::{Event, EventKind}; -pub mod statistics; - -use statistics::gamedata::{self, Boss}; +pub mod gamedata; +pub use gamedata::Boss;  use std::fmt; diff --git a/src/statistics/boon.rs b/src/statistics/boon.rs deleted file mode 100644 index 196bd1d..0000000 --- a/src/statistics/boon.rs +++ /dev/null @@ -1,361 +0,0 @@ -//! Module providing functions and structs to deal with boon related statistics. -use std::cmp; -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 { -    /// Boon stacks duration, e.g. Regeneration. -    Duration, -    /// Boon stacks intensity, e.g. Might. -    Intensity, -} - -/// A struct that helps with simulating boon changes over time. -/// -/// This basically simulates a single boon-queue (for a single boon). -/// -/// # A quick word about how boon queues work -/// -/// For each boon, you have an internal *boon queue*, limited to a specific -/// capacity. When the current stack expires, the next one is taken from the -/// queue. -/// -/// The queue is sorted by boon strength. This means that "weak" boons are -/// always at the end (and as such, are the first ones to be deleted when the -/// queue is full). This prevents "bad" boons (e.g. the Quickness from Lightning -/// Hammer #2) to override the "good" boons (e.g. the Quickness from your -/// friendly neighborhood Chrono with 100% boon duration). -/// -/// This also means that boons can be "lost". If the queue is full, the boon -/// might not get applied, or it might replace another boon, thus wasting some -/// of the boon duration. -/// -/// Intensity-stacked boons (such as Might) work a bit differently: as time -/// passes, all stacks are decreased simultaneously! As soon as a stack reaches -/// 0, it is dropped. -/// -/// You can find more information and the size of some of the queues on the wiki: -/// https://wiki.guildwars2.com/wiki/Effect_stacking -#[derive(Clone, Debug)] -pub struct BoonQueue { -    capacity: u32, -    queue: Vec<u64>, -    boon_type: BoonType, -    next_update: u64, -    backlog: u64, -} - -impl BoonQueue { -    /// Create a new boon queue. -    /// -    /// * `capacity` - The capacity of the queue. -    /// * `boon_type` - How the boons stack. -    pub fn new(capacity: u32, boon_type: BoonType) -> BoonQueue { -        BoonQueue { -            capacity, -            queue: Vec::new(), -            boon_type, -            next_update: 0, -            backlog: 0, -        } -    } - -    fn fix_queue(&mut self) { -        // Sort reversed, so that the longest stack is at the front. -        self.queue.sort_unstable_by(|a, b| b.cmp(a)); -        // Truncate queue by cutting of the shortest stacks -        if self.queue.len() > self.capacity as usize { -            self.queue.drain(self.capacity as usize..); -        } -    } - -    /// Get the type of this boon. -    pub fn boon_type(&self) -> BoonType { -        self.boon_type -    } - -    /// Add a boon stack to this queue. -    /// -    /// * `duration` - Duration (in milliseconds) of the added stack. -    pub fn add_stack(&mut self, duration: u64) { -        let backlog = self.backlog; -        self.do_simulate(backlog); -        self.queue.push(duration); -        self.fix_queue(); -        self.next_update = self.next_update(); -    } - -    /// Return the amount of current stacks. -    /// -    /// If the boon type is a duration boon, this will always return 0 or 1. -    /// -    /// If the boon type is an intensity boon, it will return the number of -    /// stacks. -    pub fn current_stacks(&self) -> u32 { -        let result = match self.boon_type { -            BoonType::Intensity => self.queue.len(), -            BoonType::Duration => cmp::min(1, self.queue.len()), -        }; -        result as u32 -    } - -    /// Simulate time passing. -    /// -    /// This will decrease the remaining duration of the stacks accordingly. -    /// -    /// * `duration` - The amount of time (in milliseconds) to simulate. -    pub fn simulate(&mut self, duration: u64) { -        if duration == 0 { -            return; -        } -        if duration < self.next_update { -            self.next_update -= duration; -            self.backlog += duration; -        } else { -            let total = self.backlog + duration; -            self.do_simulate(total); -        } -    } - -    /// Simulate the thing without using the backlog. -    fn do_simulate(&mut self, duration: u64) { -        let mut remaining = duration; -        match self.boon_type { -            BoonType::Duration => { -                while remaining > 0 && !self.queue.is_empty() { -                    let next = self.queue.remove(0); -                    if next > remaining { -                        self.queue.push(next - remaining); -                        break; -                    } else { -                        remaining -= next; -                    } -                } -                self.fix_queue(); -            } - -            BoonType::Intensity => { -                self.queue = self -                    .queue -                    .iter() -                    .cloned() -                    .filter(|v| *v > duration) -                    .map(|v| v - duration) -                    .collect(); -            } -        } -        self.next_update = self.next_update(); -        self.backlog = 0; -    } - -    /// Remove all stacks. -    pub fn clear(&mut self) { -        self.queue.clear(); -        self.next_update = 0; -        self.backlog = 0; -    } - -    /// Cleanse a single stack -    pub fn drop_single(&mut self) { -        if self.is_empty() { -            return; -        } -        self.queue.pop(); -    } - -    /// Checks if any stacks are left. -    pub fn is_empty(&self) -> bool { -        self.queue.is_empty() -    } - -    /// Calculate when the stacks will have the next visible change. -    /// -    /// This assumes that the stacks will not be modified during this time. -    /// -    /// The return value is the duration in milliseconds. If the boon queue is -    /// currently empty, 0 is returned. -    pub fn next_change(&self) -> u64 { -        match self.boon_type { -            BoonType::Duration => self.queue.iter().sum(), -            BoonType::Intensity => self.queue.last().cloned().unwrap_or(0), -        } -    } - -    /// Calculate when the boon queue should be updated next. -    /// -    /// The next update always means that a stack runs out, even if it has no -    /// visible effect. -    /// -    /// For each queue: `next_update() <= next_change()`. -    /// -    /// A return value of 0 means that there's no update awaiting. -    pub fn next_update(&self) -> u64 { -        self.queue.last().cloned().unwrap_or(0) -    } -} - -/// 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<u64> 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<u32, RecordFunc<u64, (), Stacks>>, -} - -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: u32, 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: u32) -> f32 { -        assert!(b >= a, "timespan is negative?!"); -        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: u32) -> u32 { -        self.inner -            .get(&boon_id) -            .map(|f| f.get(&x)) -            .unwrap_or(Stacks(0)) -            .0 as u32 -    } -} - -impl fmt::Debug for BoonLog { -    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { -        write!(f, "BoonLog {{ .. }}") -    } -} - -#[cfg(test)] -mod test { -    use super::*; - -    #[test] -    fn test_queue_capacity() { -        let mut queue = BoonQueue::new(5, BoonType::Intensity); -        assert_eq!(queue.current_stacks(), 0); -        for _ in 0..10 { -            queue.add_stack(10); -        } -        assert_eq!(queue.current_stacks(), 5); -    } - -    #[test] -    fn test_simulate_duration() { -        let mut queue = BoonQueue::new(10, BoonType::Duration); -        queue.add_stack(10); -        queue.add_stack(20); -        assert_eq!(queue.current_stacks(), 1); -        queue.simulate(30); -        assert_eq!(queue.current_stacks(), 0); - -        queue.add_stack(50); -        queue.simulate(30); -        assert_eq!(queue.current_stacks(), 1); -        queue.simulate(10); -        assert_eq!(queue.current_stacks(), 1); -        queue.simulate(15); -        assert_eq!(queue.current_stacks(), 0); -    } - -    #[test] -    fn test_simulate_intensity() { -        let mut queue = BoonQueue::new(5, BoonType::Intensity); - -        queue.add_stack(10); -        queue.add_stack(20); -        assert_eq!(queue.current_stacks(), 2); - -        queue.simulate(5); -        assert_eq!(queue.current_stacks(), 2); - -        queue.simulate(5); -        assert_eq!(queue.current_stacks(), 1); -        queue.simulate(15); -        assert_eq!(queue.current_stacks(), 0); -    } -} diff --git a/src/statistics/damage.rs b/src/statistics/damage.rs deleted file mode 100644 index 0c26a9b..0000000 --- a/src/statistics/damage.rs +++ /dev/null @@ -1,95 +0,0 @@ -use super::math::{Monoid, RecordFunc, Semigroup}; -use std::fmt; - -/// Type of the damage. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum DamageType { -    Physical, -    Condition, -} - -/// Meta information about a damage log entry. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub struct Meta { -    pub source: u64, -    pub target: u64, -    pub kind: DamageType, -    pub skill: u32, -} - -/// A small wrapper that wraps a damage number. -#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] -pub struct Damage(pub u64); - -impl Semigroup for Damage { -    #[inline] -    fn combine(&self, other: &Self) -> Self { -        Damage(self.0 + other.0) -    } -} - -impl Monoid for Damage { -    #[inline] -    fn mempty() -> Self { -        Damage(0) -    } -} - -/// Provides access to the damage log. -#[derive(Clone, Default)] -pub struct DamageLog { -    inner: RecordFunc<u64, Meta, Damage>, -} - -impl DamageLog { -    pub fn new() -> Self { -        DamageLog { -            inner: RecordFunc::new(), -        } -    } - -    pub fn log( -        &mut self, -        time: u64, -        source: u64, -        target: u64, -        kind: DamageType, -        skill: u32, -        value: u64, -    ) { -        self.inner.insert( -            time, -            Meta { -                source, -                target, -                kind, -                skill, -            }, -            Damage(value), -        ) -    } - -    pub fn damage_between<F: FnMut(&Meta) -> bool>( -        &self, -        start: u64, -        stop: u64, -        filter: F, -    ) -> Damage { -        self.inner.between_only(&start, &stop, filter) -    } - -    pub fn damage<F: FnMut(&Meta) -> bool>(&self, filter: F) -> Damage { -        self.inner.tally_only(filter) -    } -} - -impl fmt::Debug for DamageLog { -    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { -        write!( -            f, -            "DamageLog {{ {} events logged, {:?} total damage }}", -            self.inner.len(), -            self.inner.tally() -        ) -    } -} diff --git a/src/statistics/gamedata.rs b/src/statistics/gamedata.rs deleted file mode 100644 index d942939..0000000 --- a/src/statistics/gamedata.rs +++ /dev/null @@ -1,294 +0,0 @@ -//! This module contains some game data that is necessary to correctly calculate -//! some statistics. -use std::{fmt, str::FromStr}; -use super::boon::{BoonQueue, BoonType}; -use num_derive::FromPrimitive; - -/// Enum containing all bosses with their IDs. -#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, FromPrimitive)] -pub enum Boss { -    // Wing 1 -    ValeGuardian = 0x3C4E, -    Gorseval = 0x3C45, -    Sabetha = 0x3C0F, - -    // Wing 2 -    Slothasor = 0x3EFB, -    Matthias = 0x3EF3, - -    // Wing 3 -    KeepConstruct = 0x3F6B, -    /// Xera ID for phase 1. -    /// -    /// This is only half of Xera's ID, as there will be a second agent for the -    /// second phase. This agent will have another ID, see -    /// [`XERA_PHASE2_ID`](constant.XERA_PHASE2_ID.html). -    Xera = 0x3F76, - -    // Wing 4 -    Cairn = 0x432A, -    MursaatOverseer = 0x4314, -    Samarog = 0x4324, -    Deimos = 0x4302, - -    // Wing 5 -    SoullessHorror = 0x4D37, -    Dhuum = 0x4BFA, - -    // Wing 6 -    ConjuredAmalgamate = 0xABC6, -    LargosTwins = 0x5271, -    Qadim = 0x51C6, - -    // Wing 7 -    CardinalAdina = 0x55F6, -    CardinalSabir = 0x55CC, -    QadimThePeerless = 0x55F0, - -    // 100 CM -    Skorvald = 0x44E0, -    Artsariiv = 0x461D, -    Arkk = 0x455F, - -    // 99 CM -    MAMA = 0x427D, -    Siax = 0x4284, -    Ensolyss = 0x4234, - -    // Strike missions -    IcebroodConstruct = 0x568A, -    VoiceOfTheFallen = 0x5747, -    FraenirOfJormag = 0x57DC, -    Boneskinner = 0x57F9, -    WhisperOfJormag = 0x58B7, -} - - -/// Error for when converting a string to the boss fails. -#[derive(Debug, Clone, Hash, PartialEq, Eq)] -pub struct ParseBossError(String); - - -impl fmt::Display for ParseBossError { -    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { -        write!(f, "Invalid boss identifier: {}", self.0) -    } -} - - -impl FromStr for Boss { -    type Err = ParseBossError; - -    fn from_str(s: &str) -> Result<Self, Self::Err> { -        let lower = s.to_lowercase(); -        match &lower as &str { -            "vg" | "vale guardian" => Ok(Boss::ValeGuardian), -            "gorse" | "gorseval" => Ok(Boss::Gorseval), -            "sab" | "sabetha" => Ok(Boss::Sabetha), - -            "sloth" | "slothasor" => Ok(Boss::Slothasor), -            "matthias" => Ok(Boss::Matthias), - -            "kc" | "keep construct" => Ok(Boss::KeepConstruct), -            "xera" => Ok(Boss::Xera), - -            "cairn" => Ok(Boss::Cairn), -            "mo" | "mursaat overseer" => Ok(Boss::MursaatOverseer), -            "sam" | "sama" | "samarog" => Ok(Boss::Samarog), -            "deimos" => Ok(Boss::Deimos), - -            "desmina" | "sh" => Ok(Boss::SoullessHorror), -            "dhuum" => Ok(Boss::Dhuum), - -            "ca" | "conjured almagamate" => Ok(Boss::ConjuredAmalgamate), -            "largos" | "twins" => Ok(Boss::LargosTwins), -            "qadim" => Ok(Boss::Qadim), - -            "adina" | "cardinal adina" => Ok(Boss::CardinalAdina), -            "sabir" | "cardinal sabir" => Ok(Boss::CardinalSabir), -            "qadimp" | "peerless qadim" | "qadim the peerless" => Ok(Boss::QadimThePeerless), - -            "skorvald" => Ok(Boss::Skorvald), -            "artsariiv" => Ok(Boss::Artsariiv), -            "arkk" => Ok(Boss::Arkk), - -            "mama" => Ok(Boss::MAMA), -            "siax" => Ok(Boss::Siax), -            "ensolyss" => Ok(Boss::Ensolyss), - -            "icebrood" | "icebrood construct" => Ok(Boss::IcebroodConstruct), -            "super kodan brothers" => Ok(Boss::VoiceOfTheFallen), -            "fraenir" | "fraenir of jormag" => Ok(Boss::FraenirOfJormag), -            "boneskinner" => Ok(Boss::Boneskinner), -            "whisper" | "whisper of jormag" => Ok(Boss::WhisperOfJormag), - -            _ => Err(ParseBossError(s.to_owned())) -        } -    } -} - - -/// ID for Xera in the second phase. -/// -/// The original Xera will despawn, and after the tower phase, a separate spawn -/// will take over. This new Xera will have this ID. Care needs to be taken when -/// calculating boss damage on this encounter, as both Xeras have to be taken -/// into account. -pub const XERA_PHASE2_ID: u16 = 0x3F9E; - -/// Contains a boon. -/// -/// Fields: -/// * boon id -/// * name (english) (just for easier debugging) -/// * maximum number of stacks -/// * boon type (intensity or duration) -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct Boon(pub u32, pub &'static str, pub u32, pub BoonType); - -impl Boon { -    pub fn create_queue(&self) -> BoonQueue { -        BoonQueue::new(self.2, self.3) -    } -} - -/// A list of all boons (and conditions) -pub static BOONS: &[Boon] = &[ -    // Standard boons. -    // Boon queue sizes taken from the wiki: -    // https://wiki.guildwars2.com/wiki/Effect_stacking -    // IDs from wiki and skilldef.log: -    // https://www.deltaconnected.com/arcdps/evtc/ - -    // Duration based -    Boon(743, "Aegis", 5, BoonType::Duration), -    Boon(30328, "Alacrity", 9, BoonType::Duration), -    Boon(725, "Fury", 9, BoonType::Duration), -    Boon(717, "Protection", 5, BoonType::Duration), -    Boon(718, "Regeneration", 5, BoonType::Duration), -    Boon(26980, "Resistance", 5, BoonType::Duration), -    Boon(873, "Retaliation", 5, BoonType::Duration), -    Boon(719, "Swiftness", 9, BoonType::Duration), -    Boon(1187, "Quickness", 5, BoonType::Duration), -    Boon(726, "Vigor", 5, BoonType::Duration), -    // Intensity based -    Boon(740, "Might", 25, BoonType::Intensity), -    Boon(1122, "Stability", 25, BoonType::Intensity), -    // Standard conditions. -    // Duration based -    Boon(720, "Blinded", 5, BoonType::Duration), -    Boon(722, "Chilled", 5, BoonType::Duration), -    Boon(721, "Crippled", 5, BoonType::Duration), -    Boon(791, "Fear", 5, BoonType::Duration), -    Boon(727, "Immobile", 3, BoonType::Duration), -    Boon(26766, "Slow", 3, BoonType::Duration), -    Boon(742, "Weakness", 3, BoonType::Duration), -    // Intensity based -    Boon(736, "Bleeding", 1500, BoonType::Intensity), -    Boon(737, "Burning", 1500, BoonType::Intensity), -    Boon(861, "Confusion", 1500, BoonType::Intensity), -    Boon(723, "Poison", 1500, BoonType::Intensity), -    Boon(19426, "Torment", 1500, BoonType::Intensity), -    Boon(738, "Vulnerability", 25, BoonType::Intensity), -]; - -pub fn get_boon(boon_id: u32) -> Option<&'static Boon> { -    BOONS.iter().find(|b| b.0 == boon_id) -} - -/// Contains pre-defined triggers for boss mechanics. -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub enum Trigger { -    /// Triggers when the given boon is applied to the player. -    BoonPlayer(u32), -    /// Triggers when the given boon is applied to the boss. -    BoonBoss(u32), -    /// Triggers when the given skill is used by a player. -    SkillByPlayer(u32), -    /// Triggers when the given skill is used on a player. -    SkillOnPlayer(u32), -    /// Triggers when the given boon is stripped from an enemy. -    BoonStripped(u32), -    /// Triggers when the given entity spawned. -    Spawn(u16), -    /// Triggers when the boss finishes channeling the given skill. -    ChannelComplete(u32), -} - -/// Struct describing a boss mechanic. -/// -/// Fields: -/// * Boss id that this mechanic belongs to. -/// * How the mechanic is triggered. -/// * Technical term for the mechanic (for debugging purposes). -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct Mechanic(pub u16, pub Trigger, pub &'static str); - -impl Mechanic { -    #[inline] -    pub fn boss_id(&self) -> u16 { -        self.0 -    } - -    #[inline] -    pub fn trigger(&self) -> &Trigger { -        &self.1 -    } - -    #[inline] -    pub fn name(&self) -> &'static str { -        self.2 -    } -} - -macro_rules! mechanics { -    ( $( $boss_id:expr => [ $($name:expr => $trigger:expr,)* ], )* ) => { -        &[ -            $( $(Mechanic($boss_id as u16, $trigger, $name)),* ),* -         ] -    } -} - -/// A slice of all mechanics that we know about. -pub static MECHANICS: &[Mechanic] = mechanics! { -    // Wing 1 -    Boss::ValeGuardian => [ -        // Teleport: -        "Unstable Magic Spike" => Trigger::SkillOnPlayer(31392), -    ], -    Boss::Gorseval => [ -        // Slam -        "Spectral Impact" => Trigger::SkillOnPlayer(31875), -        // Egg -        "Ghastly Prison" => Trigger::BoonPlayer(31623), -    ], -    Boss::Sabetha => [ -        // Took the launch pad -        "Shell-Shocked" => Trigger::BoonPlayer(34108), -    ], - -    // Wing 4 -    Boss::Samarog => [ -        "Prisoner Sweep" => Trigger::SkillOnPlayer(38168), -        "Shockwave" => Trigger::SkillOnPlayer(37996), -    ], -}; - -/// Get all mechanics that belong to the given boss. -pub fn get_mechanics(boss_id: u16) -> Vec<&'static Mechanic> { -    MECHANICS.iter().filter(|m| m.0 == boss_id).collect() -} - - -#[cfg(test)] -mod tests { -    use super::*; - -    #[test] -    pub fn test_parsing() { -        assert_eq!("vg".parse(), Ok(Boss::ValeGuardian)); -        assert_eq!("VG".parse(), Ok(Boss::ValeGuardian)); - -        assert!("vga".parse::<Boss>().is_err()); -    } -} diff --git a/src/statistics/math.rs b/src/statistics/math.rs deleted file mode 100644 index a0849a3..0000000 --- a/src/statistics/math.rs +++ /dev/null @@ -1,240 +0,0 @@ -//! 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. -pub trait Semigroup { -    fn combine(&self, other: &Self) -> Self; -} - -/// A monoid. -/// -/// Extends the semigroup with a "neutral" element. -/// -/// # Laws -/// -/// ```raw -/// mempty.combine(x) == x -/// x.combine(mempty) == x -/// ``` -pub trait Monoid: Semigroup { -    fn mempty() -> Self; -} - -#[derive(Debug, Clone)] -#[doc(hidden)] -pub struct Record<X, T, D> { -    x: X, -    tag: T, -    data: D, -} - -/// A function that records tagged data points. -/// -/// This represents a "function" as a list of increases at soem discrete points. -/// Think about it as a generalized damage log. Increases can be tagged by some -/// arbitrary data, for example which the agent ID, the skill ID, the target, -/// ... -/// -/// This offers methods to get the value at a specific point (by "summing up" -/// all increments before that point), between two points and in total. It also -/// offers variants that allow you to filter the increments by their tag. -/// -/// Type parameters: -/// -/// * `X` domain of the function. Must have a defined `Ord`ering. -/// * `T` tag for each data point. Can be arbitrary. -/// * `D` actual data. Must be [`Monoid`](trait.Monoid.html), so that it can be -///   summed up. -#[derive(Clone)] -pub struct RecordFunc<X, T, D> { -    data: Vec<Record<X, T, D>>, -} - -impl<X, T, D> RecordFunc<X, T, D> -where -    X: Ord, -    D: Monoid, -{ -    /// Create a new `RecordFunc`. -    pub fn new() -> Self { -        RecordFunc { data: Vec::new() } -    } - -    #[doc(hidden)] -    pub fn data(&self) -> &[Record<X, T, D>] { -        &self.data -    } - -    /// Insert a data point into the record func. -    /// -    /// Note that you should supply the *increment*, not the *absolute value*! -    pub fn insert(&mut self, x: X, tag: T, data: D) { -        // Usually, the list will be built up in order, which means we can -        // always append to the end. Check for this special case to make it -        // faster. -        if self.data.last().map(|r| r.x < x).unwrap_or(true) { -            self.data.push(Record { x, tag, data }); -        } else { -            let index = match self.data.binary_search_by(|r| r.x.cmp(&x)) { -                Ok(i) => i, -                Err(i) => i, -            }; -            self.data.insert(index, Record { x, tag, data }); -        } -        //self.data.sort_by(|a, b| a.x.cmp(&b.x)); -    } - -    /// Get the amount of data points saved. -    pub fn len(&self) -> usize { -        self.data.len() -    } - -    /// Check whether there are no records. -    pub fn is_emtpy(&self) -> bool { -        self.data.is_empty() -    } - -    /// Get the absolute value at the specific point. -    #[inline] -    pub fn get(&self, x: &X) -> D { -        self.get_only(x, |_| true) -    } - -    /// Get the absolute value at the specific point by only considering -    /// increments where the predicate holds. -    pub fn get_only<F: FnMut(&T) -> bool>(&self, x: &X, mut predicate: F) -> D { -        self.data -            .iter() -            .take_while(|record| record.x <= *x) -            .filter(|record| predicate(&record.tag)) -            .fold(D::mempty(), |a, b| a.combine(&b.data)) -    } - -    /// Get the increments between the two given points. -    #[inline] -    pub fn between(&self, a: &X, b: &X) -> D { -        self.between_only(a, b, |_| true) -    } - -    /// Get the increments between the two given points by only considering -    /// increments where the predicate holds. -    pub fn between_only<F: FnMut(&T) -> bool>(&self, a: &X, b: &X, mut predicate: F) -> D { -        self.data -            .iter() -            .skip_while(|record| record.x < *a) -            .take_while(|record| record.x <= *b) -            .filter(|record| predicate(&record.tag)) -            .fold(D::mempty(), |a, b| a.combine(&b.data)) -    } - -    /// Get the sum of all increments. -    #[inline] -    pub fn tally(&self) -> D { -        self.tally_only(|_| true) -    } - -    /// Get the sum of all increments by only considering increments where the -    /// predicate holds. -    pub fn tally_only<F: FnMut(&T) -> bool>(&self, mut predicate: F) -> D { -        self.data -            .iter() -            .filter(|record| predicate(&record.tag)) -            .fold(D::mempty(), |a, b| a.combine(&b.data)) -    } -} - -impl<X, T, D, A> RecordFunc<X, T, D> -where -    X: Ord + Sub<X, Output = X> + Copy, -    D: Monoid + Mul<X, Output = A>, -    A: Monoid, -{ -    #[inline] -    pub fn integral(&self, a: &X, b: &X) -> A { -        self.integral_only(a, b, |_| true) -    } - -    pub fn integral_only<F: FnMut(&T) -> 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::<Vec<_>>(); -        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<X: Ord, T, D: Monoid> Default for RecordFunc<X, T, D> { -    fn default() -> Self { -        Self::new() -    } -} - -#[cfg(test)] -mod test { -    use super::*; - -    #[derive(Debug, PartialEq, Eq)] -    struct Integer(u32); - -    impl Semigroup for Integer { -        fn combine(&self, other: &Self) -> Self { -            Integer(self.0 + other.0) -        } -    } - -    impl Monoid for Integer { -        fn mempty() -> Self { -            Integer(0) -        } -    } - -    fn create() -> RecordFunc<u32, u8, Integer> { -        let mut result = RecordFunc::new(); - -        result.insert(6, 1, Integer(6)); -        result.insert(4, 0, Integer(5)); -        result.insert(0, 1, Integer(3)); -        result.insert(2, 0, Integer(4)); - -        result -    } - -    #[test] -    fn recordfunc_get() { -        let rf = create(); - -        assert_eq!(rf.get(&3), Integer(7)); -        assert_eq!(rf.get(&4), Integer(12)); -    } - -    #[test] -    fn recordfunc_get_only() { -        let rf = create(); - -        assert_eq!(rf.get_only(&3, |t| *t == 0), Integer(4)); -    } - -    #[test] -    fn recordfunc_between() { -        let rf = create(); - -        assert_eq!(rf.between(&1, &5), Integer(9)); -    } -} diff --git a/src/statistics/mechanics.rs b/src/statistics/mechanics.rs deleted file mode 100644 index 0cf6f24..0000000 --- a/src/statistics/mechanics.rs +++ /dev/null @@ -1,62 +0,0 @@ -use super::gamedata::Mechanic; -use super::math::{Monoid, RecordFunc, Semigroup}; - -use std::fmt; - -/// A simple wrapper for integers. -#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] -struct Counter(u32); - -impl Semigroup for Counter { -    #[inline] -    fn combine(&self, other: &Counter) -> Counter { -        Counter(self.0 + other.0) -    } -} - -impl Monoid for Counter { -    #[inline] -    fn mempty() -> Counter { -        Counter(0) -    } -} - -/// Provides access to the mechanic log. -#[derive(Clone, Default)] -pub struct MechanicLog { -    inner: RecordFunc<u64, (&'static Mechanic, u64), Counter>, -} - -impl MechanicLog { -    /// Increase the mechanic counter for the given mechanic and agent by one. -    pub fn increase(&mut self, time: u64, mechanic: &'static Mechanic, agent: u64) { -        self.inner.insert(time, (mechanic, agent), Counter(1)); -    } - -    /// Return the count of mechanics. -    /// -    /// A function can be provided to filter entries by mechanic type and agent. -    pub fn count<F: FnMut(&'static Mechanic, u64) -> bool>(&self, mut filter: F) -> u32 { -        self.inner.tally_only(|(a, b)| filter(a, *b)).0 -    } - -    /// Return the count of mechanics between the two given times. -    /// -    /// A function can be provided to filter entries by mechanic type and agent. -    pub fn count_between<F: FnMut(&'static Mechanic, u64) -> bool>( -        &self, -        start: u64, -        stop: u64, -        mut filter: F, -    ) -> u32 { -        self.inner -            .between_only(&start, &stop, |(a, b)| filter(a, *b)) -            .0 -    } -} - -impl fmt::Debug for MechanicLog { -    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { -        write!(f, "MechanicLog {{ ... }}") -    } -} diff --git a/src/statistics/mod.rs b/src/statistics/mod.rs deleted file mode 100644 index 3e42d9c..0000000 --- a/src/statistics/mod.rs +++ /dev/null @@ -1,145 +0,0 @@ -//! This module aids in the creation of actual boss statistics. -use super::*; -use std::collections::HashMap; -use thiserror::Error; - -pub mod boon; -pub mod damage; -pub mod gamedata; -pub mod math; -pub mod mechanics; -pub mod trackers; - -use self::boon::BoonLog; -use self::damage::DamageLog; -use self::mechanics::MechanicLog; -use self::trackers::{RunnableTracker, Tracker}; - -pub type StatResult<T> = Result<T, StatError>; - -#[derive(Error, Debug)] -pub enum StatError { -    #[error("tracker returned an error: {0}")] -    TrackerError(#[from] Box<dyn std::error::Error>), -} - -macro_rules! try_tracker { -    ($expr:expr) => { -        #[allow(unreachable_code)] -        match $expr { -            Ok(e) => e, -            Err(e) => return Err(StatError::TrackerError(e)), -        } -    }; -} - -/// A struct containing the calculated statistics for the log. -#[derive(Clone, Debug)] -pub struct Statistics { -    /// The complete damage log. -    pub damage_log: DamageLog, -    /// The complete mechanics log. -    pub mechanic_log: MechanicLog, -    /// A map mapping agent addresses to their stats. -    pub agent_stats: HashMap<u64, AgentStats>, -} - -/// A struct describing the agent statistics. -#[derive(Clone, Debug, Default)] -pub struct AgentStats { -    /// Average stacks of boons. -    /// -    /// This also includes conditions. -    /// -    /// For duration-based boons, the average amount of stacks is the same as -    /// the uptime. -    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). -    pub exit_combat: u64, -} - -impl AgentStats { -    /// Returns the combat time of this agent in milliseconds. -    pub fn combat_time(&self) -> u64 { -        self.exit_combat - self.enter_combat -    } -} - -/// Takes a bunch of trackers and runs them on the given log. -/// -/// This method returns "nothing", as the statistics are saved in the trackers. -/// It's the job of the caller to extract them appropriately. -pub fn run_trackers(log: &Log, trackers: &mut [&mut dyn RunnableTracker]) -> StatResult<()> { -    for event in log.events() { -        for tracker in trackers.iter_mut() { -            try_tracker!((*tracker).run_feed(event)); -        } -    } -    Ok(()) -} - -/// Calculate the statistics for the given log. -pub fn calculate(log: &Log) -> StatResult<Statistics> { -    let mut agent_stats = HashMap::<u64, AgentStats>::new(); - -    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(); - -    let mechanics = gamedata::get_mechanics(log.boss_id); -    let boss_addr = log.boss_agents().into_iter().map(|x| *x.addr()).collect(); -    let mut mechanic_tracker = trackers::MechanicTracker::new(boss_addr, mechanics); - -    run_trackers( -        log, -        &mut [ -            &mut damage_tracker, -            &mut log_start_tracker, -            &mut combat_time_tracker, -            &mut boon_tracker, -            &mut mechanic_tracker, -        ], -    )?; - -    let log_start_time = try_tracker!(log_start_tracker.finalize()); - -    let combat_times = try_tracker!(combat_time_tracker.finalize()); -    for (agent_addr, &(enter_time, exit_time)) in &combat_times { -        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; -        } else { -            agent.enter_combat = log_start_time; -        } -        if exit_time != 0 { -            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()); -    let mechanic_log = try_tracker!(mechanic_tracker.finalize()); - -    Ok(Statistics { -        damage_log, -        mechanic_log, -        agent_stats, -    }) -} diff --git a/src/statistics/trackers.rs b/src/statistics/trackers.rs deleted file mode 100644 index 16cb755..0000000 --- a/src/statistics/trackers.rs +++ /dev/null @@ -1,443 +0,0 @@ -//! evtclib tracker definitions. -//! -//! The idea behind a "tracker" is to have one object taking care of one -//! specific thing. This makes it easier to organize the whole "statistic -//! gathering loop", and it keeps each tracker somewhat small. -//! -//! It's also easy to define your own trackers if there are any statistics that -//! you want to track. Just implement [`Tracker`](trait.Tracker.html). It -//! doesn't matter what you track, it doesn't matter how many trackers you -//! define. -//! -//! If you want to track stats separated by player or phases, consider writing -//! your tracker in a way that it only tracks statistics for a single player, -//! and then use a [`Multiplexer`](struct.Multiplexer.html) to automatically -//! track it for every player/agent. -//! -//! You can use [`run_trackers`](../fn.run_trackers.html) to run multiple -//! trackers on the same log. -use std::collections::HashMap; -use std::error::Error; - -use super::super::{Event, EventKind, Log}; -use super::boon::{BoonLog, BoonQueue}; -use super::damage::{DamageLog, DamageType}; -use super::gamedata::{self, Mechanic, Trigger}; -use super::mechanics::MechanicLog; - -use super::super::raw::CbtResult; - -use fnv::FnvHashMap; - -/// A tracker. -/// -/// A tracker should be responsible for tracking a single statistic. Each -/// tracker is fed each event. If an error is returned while feeding, the whole -/// calculation will be aborted. -pub trait Tracker { -    /// The resulting statistic that this tracker will return. -    type Stat; -    /// The error that this tracker might return. -    type Error: Error; - -    /// Feed a single event into this tracker. -    /// -    /// The tracker will update its internal state. -    fn feed(&mut self, event: &Event) -> Result<(), Self::Error>; - -    /// Finalize this tracker and get the statistics out. -    fn finalize(self) -> Result<Self::Stat, Self::Error>; -} - -/// A helper trait that erases the types from a tracker. -/// -/// This makes it able to use references like `&mut RunnableTracker` without -/// having to specify the generic types, allowing you to e.g. store a bunch of -/// them in a vector, regardless of their output type. Unless you want to do -/// that, you probably don't want to use this trait directly. -/// -/// Note that you do not need to implement this yourself. It is automatically -/// implemented for all types that implement `Tracker`. -/// -/// RunnableTrackers provide no way to extract their resources, and they wrap -/// all errors in `Box<_>`, so you should always keep a "real" reference around -/// if you plan on getting any data. -pub trait RunnableTracker { -    /// See `Tracker.feed()`. Renamed to avoid conflicts. -    fn run_feed(&mut self, event: &Event) -> Result<(), Box<dyn Error>>; -} - -impl<S, E: Error + 'static, T: Tracker<Stat = S, Error = E>> RunnableTracker for T { -    fn run_feed(&mut self, event: &Event) -> Result<(), Box<dyn Error>> { -        self.feed(event).map_err(|e| Box::new(e) as Box<dyn Error>) -    } -} - -/// A tracker that tracks per-target damage of all agents. -pub struct DamageTracker<'l> { -    log: &'l Log, -    damage_log: DamageLog, -} - -impl<'t> DamageTracker<'t> { -    /// Create a new damage tracker for the given log. -    pub fn new(log: &Log) -> DamageTracker { -        DamageTracker { -            log, -            damage_log: DamageLog::new(), -        } -    } -} - -impl<'t> Tracker for DamageTracker<'t> { -    type Stat = DamageLog; -    type Error = !; - -    fn feed(&mut self, event: &Event) -> Result<(), Self::Error> { -        match event.kind { -            EventKind::Physical { -                source_agent_addr, -                destination_agent_addr, -                damage, -                skill_id, -                .. -            } => { -                let source = if let Some(master) = self.log.master_agent(source_agent_addr) { -                    master.addr -                } else { -                    source_agent_addr -                }; -                self.damage_log.log( -                    event.time, -                    source, -                    destination_agent_addr, -                    DamageType::Physical, -                    skill_id, -                    damage as u64, -                ); -            } - -            EventKind::ConditionTick { -                source_agent_addr, -                destination_agent_addr, -                damage, -                condition_id, -                .. -            } => { -                let source = if let Some(master) = self.log.master_agent(source_agent_addr) { -                    master.addr -                } else { -                    source_agent_addr -                }; -                self.damage_log.log( -                    event.time, -                    source, -                    destination_agent_addr, -                    DamageType::Condition, -                    condition_id, -                    damage as u64, -                ); -            } - -            _ => (), -        } -        Ok(()) -    } - -    fn finalize(self) -> Result<Self::Stat, Self::Error> { -        Ok(self.damage_log) -    } -} - -/// Tracks when the log has been started. -#[derive(Default)] -pub struct LogStartTracker { -    event_time: u64, -} - -impl LogStartTracker { -    /// Create a new log start tracker. -    pub fn new() -> LogStartTracker { -        LogStartTracker { event_time: 0 } -    } -} - -impl Tracker for LogStartTracker { -    type Stat = u64; -    type Error = !; - -    fn feed(&mut self, event: &Event) -> Result<(), Self::Error> { -        if let EventKind::LogStart { .. } = event.kind { -            self.event_time = event.time; -        } -        Ok(()) -    } - -    fn finalize(self) -> Result<Self::Stat, Self::Error> { -        Ok(self.event_time) -    } -} - -/// A tracker that tracks the combat entry and exit times for each agent. -#[derive(Default)] -pub struct CombatTimeTracker { -    times: HashMap<u64, (u64, u64)>, -} - -impl CombatTimeTracker { -    /// Create a new combat time tracker. -    pub fn new() -> CombatTimeTracker { -        Default::default() -    } -} - -impl Tracker for CombatTimeTracker { -    // Maps from agent addr to (entry time, exit time) -    type Stat = HashMap<u64, (u64, u64)>; -    type Error = !; - -    fn feed(&mut self, event: &Event) -> Result<(), Self::Error> { -        match event.kind { -            EventKind::EnterCombat { agent_addr, .. } => { -                self.times.entry(agent_addr).or_insert((0, 0)).0 = event.time; -            } - -            EventKind::ExitCombat { agent_addr } => { -                self.times.entry(agent_addr).or_insert((0, 0)).1 = event.time; -            } - -            _ => (), -        } -        Ok(()) -    } - -    fn finalize(self) -> Result<Self::Stat, Self::Error> { -        Ok(self.times) -    } -} - -/// A tracker that tracks the total "boon area" per agent. -/// -/// The boon area is defined as the amount of stacks multiplied by the time. So -/// 1 stack of Might for 1000 milliseconds equals 1000 "stackmilliseconds" of -/// Might. You can use this boon area to calculate the average amount of stacks -/// by taking the boon area and dividing it by the combat time. -/// -/// Note that this also tracks conditions, because internally, they're handled -/// the same way. -/// -/// This tracker only tracks the boons that are known to evtclib, that is the -/// boons defined in `evtclib::statistics::gamedata::BOONS`. -pub struct BoonTracker { -    boon_logs: FnvHashMap<u64, BoonLog>, -    boon_queues: FnvHashMap<u64, FnvHashMap<u32, BoonQueue>>, -    last_time: u64, -} - -impl BoonTracker { -    /// Creates a new boon tracker for the given agent. -    pub fn new() -> BoonTracker { -        BoonTracker { -            boon_logs: Default::default(), -            boon_queues: Default::default(), -            last_time: 0, -        } -    } - -    /// Updates the internal boon queues by the given amount of milliseconds. -    /// -    /// * `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)); -    } - -    fn cleanup_queues(&mut self) { -        // 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()); -    } - -    fn update_logs(&mut self, time: u64) { -        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()); -            } -        } -    } - -    /// 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, agent_addr: u64, buff_id: u32) -> Option<&mut BoonQueue> { -        use std::collections::hash_map::Entry; -        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()), -            // Queue needs to be created, but only if we know about that boon. -            Entry::Vacant(e) => { -                let boon_queue = gamedata::get_boon(buff_id).map(gamedata::Boon::create_queue); -                if let Some(queue) = boon_queue { -                    Some(e.insert(queue)) -                } else { -                    None -                } -            } -        } -    } -} - -impl Tracker for BoonTracker { -    type Stat = HashMap<u64, BoonLog>; -    type Error = !; - -    fn feed(&mut self, event: &Event) -> Result<(), Self::Error> { -        let delta_t = event.time - self.last_time; -        self.update_queues(delta_t); - -        match event.kind { -            EventKind::BuffApplication { -                destination_agent_addr, -                buff_id, -                duration, -                .. -            } => { -                if let Some(queue) = self.get_queue(destination_agent_addr, buff_id) { -                    queue.add_stack(duration as u64); -                } -            } - -            // XXX: Properly handle SINGLE and MANUAL removal types -            EventKind::BuffRemove { -                destination_agent_addr, -                buff_id, -                .. -            } => { -                if let Some(queue) = self.get_queue(destination_agent_addr, buff_id) { -                    queue.clear(); -                } -            } - -            _ => (), -        } - -        self.update_logs(event.time); -        self.last_time = event.time; -        self.cleanup_queues(); - -        Ok(()) -    } - -    fn finalize(self) -> Result<Self::Stat, Self::Error> { -        // Convert from FnvHashMap to HashMap in order to not leak -        // implementation details. -        Ok(self.boon_logs.into_iter().collect()) -    } -} - -/// A tracker that tracks boss mechanics for each player. -pub struct MechanicTracker { -    log: MechanicLog, -    available_mechanics: Vec<&'static Mechanic>, -    boss_addresses: Vec<u64>, -} - -impl MechanicTracker { -    /// Create a new mechanic tracker that watches over the given mechanics. -    pub fn new(boss_addresses: Vec<u64>, mechanics: Vec<&'static Mechanic>) -> MechanicTracker { -        MechanicTracker { -            log: MechanicLog::default(), -            available_mechanics: mechanics, -            boss_addresses, -        } -    } -} - -impl MechanicTracker { -    fn is_boss(&self, addr: u64) -> bool { -        self.boss_addresses.contains(&addr) -    } -} - -impl Tracker for MechanicTracker { -    type Stat = MechanicLog; -    type Error = !; - -    fn feed(&mut self, event: &Event) -> Result<(), Self::Error> { -        for mechanic in &self.available_mechanics { -            match (&event.kind, &mechanic.1) { -                ( -                    EventKind::Physical { -                        source_agent_addr, -                        destination_agent_addr, -                        skill_id, -                        result, -                        .. -                    }, -                    Trigger::SkillOnPlayer(trigger_id), -                ) -                    if skill_id == trigger_id -                        && self.is_boss(*source_agent_addr) -                        && *result != CbtResult::Evade -                        && *result != CbtResult::Absorb -                        && *result != CbtResult::Block => -                { -                    self.log -                        .increase(event.time, mechanic, *destination_agent_addr); -                } - -                ( -                    EventKind::BuffApplication { -                        destination_agent_addr, -                        buff_id, -                        .. -                    }, -                    Trigger::BoonPlayer(trigger_id), -                ) -                    if buff_id == trigger_id => -                { -                    // Some buff applications are registered multiple times. So -                    // instead of counting those quick successions separately -                    // (and thus having a wrong count), we check if this -                    // mechanic has already been logged "shortly before" (10 millisecons). -                    if self -                        .log -                        .count_between(event.time - 10, event.time + 1, |m, w| { -                            &m == mechanic && w == *destination_agent_addr -                        }) -                        == 0 -                    { -                        self.log -                            .increase(event.time, mechanic, *destination_agent_addr); -                    } -                } -                _ => (), -            } -        } -        Ok(()) -    } - -    fn finalize(self) -> Result<Self::Stat, Self::Error> { -        Ok(self.log) -    } -} | 
