diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/analyzers/fractals.rs | 54 | ||||
| -rw-r--r-- | src/analyzers/helpers.rs | 70 | ||||
| -rw-r--r-- | src/analyzers/mod.rs | 65 | ||||
| -rw-r--r-- | src/analyzers/raids/mod.rs | 11 | ||||
| -rw-r--r-- | src/analyzers/raids/w4.rs | 116 | ||||
| -rw-r--r-- | src/analyzers/raids/w5.rs | 62 | ||||
| -rw-r--r-- | src/analyzers/raids/w6.rs | 87 | ||||
| -rw-r--r-- | src/analyzers/raids/w7.rs | 86 | ||||
| -rw-r--r-- | src/gamedata.rs | 59 | ||||
| -rw-r--r-- | src/lib.rs | 83 | 
10 files changed, 560 insertions, 133 deletions
| diff --git a/src/analyzers/fractals.rs b/src/analyzers/fractals.rs new file mode 100644 index 0000000..dd010ac --- /dev/null +++ b/src/analyzers/fractals.rs @@ -0,0 +1,54 @@ +//! Analyzers for (challenge mote) fractal encounters. +use crate::{ +    analyzers::{helpers, Analyzer}, +    Log, +}; + +pub const SKORVALD_CM_HEALTH: u64 = 5_551_340; + +/// Analyzer for the first boss of 100 CM, Skorvald. +/// +/// The CM is detected by the boss's health, which is higher in the challenge mote. +#[derive(Debug, Clone, Copy)] +pub struct Skorvald<'log> { +    log: &'log Log, +} + +impl<'log> Skorvald<'log> { +    pub fn new(log: &'log Log) -> Self { +        Skorvald { log } +    } +} + +impl<'log> Analyzer for Skorvald<'log> { +    fn log(&self) -> &Log { +        self.log +    } + +    fn is_cm(&self) -> bool { +        helpers::boss_health(self.log) +            .map(|h| h >= SKORVALD_CM_HEALTH) +            .unwrap_or(false) +    } +} + +#[derive(Debug, Clone, Copy)] +pub struct GenericFractal<'log> { +    log: &'log Log, +} + +impl<'log> GenericFractal<'log> { +    pub fn new(log: &'log Log) -> Self { +        GenericFractal { log } +    } +} + +impl<'log> Analyzer for GenericFractal<'log> { +    fn log(&self) -> &Log { +        self.log +    } + +    fn is_cm(&self) -> bool { +        true +    } +} diff --git a/src/analyzers/helpers.rs b/src/analyzers/helpers.rs new file mode 100644 index 0000000..ec09355 --- /dev/null +++ b/src/analyzers/helpers.rs @@ -0,0 +1,70 @@ +//! This module contains helper methods that are used in different analyzers. +use std::collections::HashMap; + +use crate::{EventKind, Log}; + +/// Returns the maximum health of the boss agent. +/// +/// If the health cannot be determined, this function returns `None`. +/// +/// The boss agent is determined by using [`Log::is_boss`][Log::is_boss]. +pub fn boss_health(log: &Log) -> Option<u64> { +    let mut health: Option<u64> = None; +    for event in log.events() { +        if let EventKind::MaxHealthUpdate { +            agent_addr, +            max_health, +        } = *event.kind() +        { +            if log.is_boss(agent_addr) { +                health = health.map(|h| h.max(max_health)).or(Some(max_health)); +            } +        } +    } +    health +} + +/// Checks if the given buff is present in the log. +pub fn buff_present(log: &Log, wanted_buff_id: u32) -> bool { +    for event in log.events() { +        if let EventKind::BuffApplication { buff_id, .. } = *event.kind() { +            if buff_id == wanted_buff_id { +                return true; +            } +        } +    } +    false +} + +/// Returns the (minimum) time between applications of the given buff in milliseconds. +pub fn time_between_buffs(log: &Log, wanted_buff_id: u32) -> u64 { +    let mut time_maps: HashMap<u64, Vec<u64>> = HashMap::new(); +    for event in log.events() { +        if let EventKind::BuffApplication { +            destination_agent_addr, +            buff_id, +            .. +        } = event.kind() +        { +            if *buff_id == wanted_buff_id { +                time_maps +                    .entry(*destination_agent_addr) +                    .or_default() +                    .push(event.time()); +            } +        } +    } +    let timestamps = if let Some(ts) = time_maps.values().max_by_key(|v| v.len()) { +        ts +    } else { +        return 0; +    }; +    timestamps +        .iter() +        .zip(timestamps.iter().skip(1)) +        .map(|(a, b)| b - a) +        // Arbitrary limit to filter out duplicated buff application events +        .filter(|x| *x > 50) +        .min() +        .unwrap_or(0) +} diff --git a/src/analyzers/mod.rs b/src/analyzers/mod.rs new file mode 100644 index 0000000..5ad88ec --- /dev/null +++ b/src/analyzers/mod.rs @@ -0,0 +1,65 @@ +//! Traits and structures to analyze fights. +//! +//! Fights need different logic to determine some data, for example each fight has a different way +//! to determine whether or not the Challenge Mote was activated, whether or not the fight was +//! successful, ... +//! +//! This module aims to unify that logic by providing a trait [`Analyzer`][Analyzer], which +//! provides a unified interface to query this information. You can use +//! [`Log::analyzer`][Log::analyzer] or [`for_log`][for_log] to obtain an analyzer fitting for the +//! encounter that is represented by the log. +//! +//! The implementation of the different analyzers is split off in different submodules: +//! * [`raids`][raids] for the raid-related encounters. +//! +//! Note that you should not create concrete analyzers on your own, as the behaviour is not +//! specified when you use a wrong analyzer for the given log. Rely only on +//! [`Log::analyzer`][Log::analyzer] (or [`for_log`][for_log]) and the methods defined in +//! [`Analyzer`][Analyzer]. + +use crate::{Boss, Log}; + +pub mod fractals; +pub mod helpers; +pub mod raids; + +/// An [`Analyzer`][Analyzer] is something that implements fight-dependent analyzing of the log. +pub trait Analyzer { +    /// Returns a reference to the log being analyzed. +    fn log(&self) -> &Log; + +    /// Checks whether the fight was done with the challenge mote activated. +    fn is_cm(&self) -> bool; +} + +/// Returns the correct [`Analyzer`][Analyzer] for the given log file. +/// +/// See also [`Log::analyzer`][Log::analyzer]. +pub fn for_log<'l>(log: &'l Log) -> Option<Box<dyn Analyzer + 'l>> { +    let boss = log.encounter()?; + +    match boss { +        Boss::Cairn => Some(Box::new(raids::Cairn::new(log))), +        Boss::MursaatOverseer => Some(Box::new(raids::MursaatOverseer::new(log))), +        Boss::Samarog => Some(Box::new(raids::Samarog::new(log))), +        Boss::Deimos => Some(Box::new(raids::Deimos::new(log))), + +        Boss::SoullessHorror => Some(Box::new(raids::SoullessHorror::new(log))), +        Boss::Dhuum => Some(Box::new(raids::Dhuum::new(log))), + +        Boss::ConjuredAmalgamate => Some(Box::new(raids::ConjuredAmalgamate::new(log))), +        Boss::LargosTwins => Some(Box::new(raids::LargosTwins::new(log))), +        Boss::Qadim => Some(Box::new(raids::Qadim::new(log))), + +        Boss::CardinalAdina => Some(Box::new(raids::CardinalAdina::new(log))), +        Boss::CardinalSabir => Some(Box::new(raids::CardinalSabir::new(log))), +        Boss::QadimThePeerless => Some(Box::new(raids::QadimThePeerless::new(log))), + +        Boss::Skorvald => Some(Box::new(fractals::Skorvald::new(log))), +        Boss::Artsariiv | Boss::Arkk | Boss::MAMA | Boss::Siax | Boss::Ensolyss => { +            Some(Box::new(fractals::GenericFractal::new(log))) +        } + +        _ => None, +    } +} diff --git a/src/analyzers/raids/mod.rs b/src/analyzers/raids/mod.rs new file mode 100644 index 0000000..91b0dba --- /dev/null +++ b/src/analyzers/raids/mod.rs @@ -0,0 +1,11 @@ +mod w4; +pub use w4::{Cairn, Deimos, MursaatOverseer, Samarog}; + +mod w5; +pub use w5::{Dhuum, SoullessHorror}; + +mod w6; +pub use w6::{ConjuredAmalgamate, LargosTwins, Qadim}; + +mod w7; +pub use w7::{CardinalAdina, CardinalSabir, QadimThePeerless}; diff --git a/src/analyzers/raids/w4.rs b/src/analyzers/raids/w4.rs new file mode 100644 index 0000000..efdab8f --- /dev/null +++ b/src/analyzers/raids/w4.rs @@ -0,0 +1,116 @@ +//! Boss fight analyzers for Wing 4 (Bastion of the Penitent). +use crate::{ +    analyzers::{helpers, Analyzer}, +    Log, +}; + +pub const CAIRN_CM_BUFF: u32 = 38_098; + +/// Analyzer for the first fight of Wing 4, Cairn. +/// +/// The CM is detected by the presence of the buff representing the countdown before which you have +/// to use your special action skill. +#[derive(Debug, Clone, Copy)] +pub struct Cairn<'log> { +    log: &'log Log, +} + +impl<'log> Cairn<'log> { +    pub fn new(log: &'log Log) -> Self { +        Cairn { log } +    } +} + +impl<'log> Analyzer for Cairn<'log> { +    fn log(&self) -> &Log { +        self.log +    } + +    fn is_cm(&self) -> bool { +        helpers::buff_present(self.log, CAIRN_CM_BUFF) +    } +} + +pub const MO_CM_HEALTH: u64 = 30_000_000; + +/// Analyzer for the second fight of Wing 4, Mursaat Overseer. +/// +/// The CM is detected by the boss's health, which is higher in the challenge mote. +#[derive(Debug, Clone, Copy)] +pub struct MursaatOverseer<'log> { +    log: &'log Log, +} + +impl<'log> MursaatOverseer<'log> { +    pub fn new(log: &'log Log) -> Self { +        MursaatOverseer { log } +    } +} + +impl<'log> Analyzer for MursaatOverseer<'log> { +    fn log(&self) -> &Log { +        self.log +    } + +    fn is_cm(&self) -> bool { +        helpers::boss_health(self.log) +            .map(|h| h >= MO_CM_HEALTH) +            .unwrap_or(false) +    } +} + +pub const SAMAROG_CM_HEALTH: u64 = 40_000_000; + +/// Analyzer for the third fight of Wing 4, Samarog. +/// +/// The CM is detected by the boss's health, which is higher in the challenge mote. +#[derive(Debug, Clone, Copy)] +pub struct Samarog<'log> { +    log: &'log Log, +} + +impl<'log> Samarog<'log> { +    pub fn new(log: &'log Log) -> Self { +        Samarog { log } +    } +} + +impl<'log> Analyzer for Samarog<'log> { +    fn log(&self) -> &Log { +        self.log +    } + +    fn is_cm(&self) -> bool { +        helpers::boss_health(self.log) +            .map(|h| h >= SAMAROG_CM_HEALTH) +            .unwrap_or(false) +    } +} + +pub const DEIMOS_CM_HEALTH: u64 = 42_000_000; + +/// Analyzer for the fourth fight of Wing 4, Deimos. +/// +/// The CM is detected by the boss's health, which is higher in the challenge mote. +#[derive(Debug, Clone, Copy)] +pub struct Deimos<'log> { +    log: &'log Log, +} + +impl<'log> Deimos<'log> { +    pub fn new(log: &'log Log) -> Self { +        Deimos { log } +    } +} + +impl<'log> Analyzer for Deimos<'log> { +    fn log(&self) -> &Log { +        self.log +    } + +    fn is_cm(&self) -> bool { +        helpers::boss_health(self.log) +            .map(|h| h >= DEIMOS_CM_HEALTH) +            .unwrap_or(false) +    } +} diff --git a/src/analyzers/raids/w5.rs b/src/analyzers/raids/w5.rs new file mode 100644 index 0000000..b8c3f3c --- /dev/null +++ b/src/analyzers/raids/w5.rs @@ -0,0 +1,62 @@ +//! Boss fight analyzers for Wing 5 (Hall of Chains) +use crate::{ +    analyzers::{helpers, Analyzer}, +    Log, +}; + +pub const DESMINA_BUFF_ID: u32 = 47414; +pub const DESMINA_MS_THRESHOLD: u64 = 11_000; + +/// Analyzer for the first fight of Wing 5, Soulless Horror (aka. Desmina). +/// +/// The CM is detected by the time between applications of the Necrosis debuff, which is applied at +/// a faster rate when the challenge mote is active. +#[derive(Debug, Clone, Copy)] +pub struct SoullessHorror<'log> { +    log: &'log Log, +} + +impl<'log> SoullessHorror<'log> { +    pub fn new(log: &'log Log) -> Self { +        SoullessHorror { log } +    } +} + +impl<'log> Analyzer for SoullessHorror<'log> { +    fn log(&self) -> &Log { +        self.log +    } + +    fn is_cm(&self) -> bool { +        let tbb = helpers::time_between_buffs(self.log, DESMINA_BUFF_ID); +        tbb > 0 && tbb <= DESMINA_MS_THRESHOLD +    } +} + +pub const DHUUM_CM_HEALTH: u64 = 40_000_000; + +/// Analyzer for the second fight of Wing 5, Dhuum. +/// +/// The CM is detected by the boss's health, which is higher in the challenge mote. +#[derive(Debug, Clone, Copy)] +pub struct Dhuum<'log> { +    log: &'log Log, +} + +impl<'log> Dhuum<'log> { +    pub fn new(log: &'log Log) -> Self { +        Dhuum { log } +    } +} + +impl<'log> Analyzer for Dhuum<'log> { +    fn log(&self) -> &Log { +        self.log +    } + +    fn is_cm(&self) -> bool { +        helpers::boss_health(self.log) +            .map(|h| h >= DHUUM_CM_HEALTH) +            .unwrap_or(false) +    } +} diff --git a/src/analyzers/raids/w6.rs b/src/analyzers/raids/w6.rs new file mode 100644 index 0000000..c4e5b1a --- /dev/null +++ b/src/analyzers/raids/w6.rs @@ -0,0 +1,87 @@ +//! Boss fight analyzers for Wing 6 (Mythwright Gambit) +use crate::{ +    analyzers::{helpers, Analyzer}, +    Log, +}; + +pub const CA_CM_BUFF: u32 = 53_075; + +/// Analyzer for the first fight of Wing 6, Conjured Amalgamate. +/// +/// The CM is detected by the presence of the buff that the player targeted by the laser has. +#[derive(Debug, Clone, Copy)] +pub struct ConjuredAmalgamate<'log> { +    log: &'log Log, +} + +impl<'log> ConjuredAmalgamate<'log> { +    pub fn new(log: &'log Log) -> Self { +        ConjuredAmalgamate { log } +    } +} + +impl<'log> Analyzer for ConjuredAmalgamate<'log> { +    fn log(&self) -> &Log { +        self.log +    } + +    fn is_cm(&self) -> bool { +        helpers::buff_present(self.log, CA_CM_BUFF) +    } +} + +pub const LARGOS_CM_HEALTH: u64 = 19_200_000; + +/// Analyzer for the second fight of Wing 6, Largos Twins. +/// +/// The CM is detected by the boss's health, which is higher in the challenge mote. +#[derive(Debug, Clone, Copy)] +pub struct LargosTwins<'log> { +    log: &'log Log, +} + +impl<'log> LargosTwins<'log> { +    pub fn new(log: &'log Log) -> Self { +        LargosTwins { log } +    } +} + +impl<'log> Analyzer for LargosTwins<'log> { +    fn log(&self) -> &Log { +        self.log +    } + +    fn is_cm(&self) -> bool { +        helpers::boss_health(self.log) +            .map(|h| h >= LARGOS_CM_HEALTH) +            .unwrap_or(false) +    } +} + +pub const QADIM_CM_HEALTH: u64 = 21_100_000; + +/// Analyzer for the third fight of Wing 6, Qadim. +/// +/// The CM is detected by the boss's health, which is higher in the challenge mote. +#[derive(Debug, Clone, Copy)] +pub struct Qadim<'log> { +    log: &'log Log, +} + +impl<'log> Qadim<'log> { +    pub fn new(log: &'log Log) -> Self { +        Qadim { log } +    } +} + +impl<'log> Analyzer for Qadim<'log> { +    fn log(&self) -> &Log { +        self.log +    } + +    fn is_cm(&self) -> bool { +        helpers::boss_health(self.log) +            .map(|h| h >= QADIM_CM_HEALTH) +            .unwrap_or(false) +    } +} diff --git a/src/analyzers/raids/w7.rs b/src/analyzers/raids/w7.rs new file mode 100644 index 0000000..a8319a3 --- /dev/null +++ b/src/analyzers/raids/w7.rs @@ -0,0 +1,86 @@ +//! Boss fight analyzers for Wing 6 (Mythwright Gambit) +use crate::{ +    analyzers::{helpers, Analyzer}, +    Log, +}; + +pub const ADINA_CM_HEALTH: u64 = 24_800_000; + +/// Analyzer for the first fight of Wing 7, Cardinal Adina. +/// +/// The CM is detected by the boss's health, which is higher in the challenge mote. +#[derive(Debug, Clone, Copy)] +pub struct CardinalAdina<'log> { +    log: &'log Log, +} + +impl<'log> CardinalAdina<'log> { +    pub fn new(log: &'log Log) -> Self { +        CardinalAdina { log } +    } +} + +impl<'log> Analyzer for CardinalAdina<'log> { +    fn log(&self) -> &Log { +        self.log +    } + +    fn is_cm(&self) -> bool { +        helpers::boss_health(self.log) +            .map(|h| h >= ADINA_CM_HEALTH) +            .unwrap_or(false) +    } +} + +pub const SABIR_CM_HEALTH: u64 = 32_400_000; + +/// Analyzer for the second fight of Wing 7, Cardinal Sabir. +/// +/// The CM is detected by the boss's health, which is higher in the challenge mote. +#[derive(Debug, Clone, Copy)] +pub struct CardinalSabir<'log> { +    log: &'log Log, +} + +impl<'log> CardinalSabir<'log> { +    pub fn new(log: &'log Log) -> Self { +        CardinalSabir { log } +    } +} + +impl<'log> Analyzer for CardinalSabir<'log> { +    fn log(&self) -> &Log { +        self.log +    } + +    fn is_cm(&self) -> bool { +        helpers::boss_health(self.log) +            .map(|h| h >= SABIR_CM_HEALTH) +            .unwrap_or(false) +    } +} + +pub const QADIMP_CM_HEALTH: u64 = 51_000_000; + +#[derive(Debug, Clone, Copy)] +pub struct QadimThePeerless<'log> { +    log: &'log Log, +} + +impl<'log> QadimThePeerless<'log> { +    pub fn new(log: &'log Log) -> Self { +        QadimThePeerless { log } +    } +} + +impl<'log> Analyzer for QadimThePeerless<'log> { +    fn log(&self) -> &Log { +        self.log +    } + +    fn is_cm(&self) -> bool { +        helpers::boss_health(self.log) +            .map(|h| h >= QADIMP_CM_HEALTH) +            .unwrap_or(false) +    } +} diff --git a/src/gamedata.rs b/src/gamedata.rs index 30bbcf6..dd11e94 100644 --- a/src/gamedata.rs +++ b/src/gamedata.rs @@ -65,42 +65,6 @@ pub enum Boss {      WhisperOfJormag = 0x58B7,  } -impl Boss { -    /// Returns the CM trigger for this boss. -    pub fn cm_trigger(self) -> CmTrigger { -        match self { -            Boss::KeepConstruct => CmTrigger::Unknown, - -            Boss::Cairn => CmTrigger::BuffPresent(38_098), -            Boss::MursaatOverseer => CmTrigger::HpThreshold(30_000_000), -            Boss::Samarog => CmTrigger::HpThreshold(40_000_000), -            Boss::Deimos => CmTrigger::HpThreshold(42_000_000), - -            Boss::SoullessHorror => CmTrigger::TimeBetweenBuffs(47414, 11_000), -            Boss::Dhuum => CmTrigger::HpThreshold(40_000_000), - -            Boss::ConjuredAmalgamate => CmTrigger::BuffPresent(53_075), -            // This is Nikare's health, as the log is saved with his ID -            Boss::LargosTwins => CmTrigger::HpThreshold(19_200_000), -            Boss::Qadim => CmTrigger::HpThreshold(21_100_000), - -            Boss::CardinalAdina => CmTrigger::HpThreshold(24_800_000), -            Boss::CardinalSabir => CmTrigger::HpThreshold(32_400_000), -            Boss::QadimThePeerless => CmTrigger::HpThreshold(51_000_000), - -            Boss::Skorvald => CmTrigger::HpThreshold(5_551_340), -            Boss::Artsariiv => CmTrigger::Always, -            Boss::Arkk => CmTrigger::Always, - -            Boss::MAMA => CmTrigger::Always, -            Boss::Siax => CmTrigger::Always, -            Boss::Ensolyss => CmTrigger::Always, - -            _ => CmTrigger::None, -        } -    } -} -  /// Error for when converting a string to the boss fails.  #[derive(Debug, Clone, Hash, PartialEq, Eq, Error)]  #[error("Invalid boss identifier: {0}")] @@ -203,29 +167,6 @@ impl Display for Boss {  /// into account.  pub const XERA_PHASE2_ID: u16 = 0x3F9E; -/// The trigger of how a boss challenge mote (CM) is determined. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum CmTrigger { -    /// The boss does not have a CM available. -    None, -    /// The boss has a CM available but we cannot determine if it has been activated. -    Unknown, -    /// Logs from this boss always count as having the CM active. -    Always, -    /// The CM is determined by the boss's health being at or above the given threshold. -    /// -    /// This works since most bosses increase their HP pool in the CM variant. -    HpThreshold(u32), -    /// The CM is active if the given buff is present in the log. -    /// -    /// The buff can be either on player or the enemy. -    BuffPresent(u32), -    /// The time between buff applications falls below the given threshold. -    /// -    /// The first number is the buff id, the second number is the time threshold in milliseconds. -    TimeBetweenBuffs(u32, u64), -} -  /// Error for when converting a string to a profession fails.  #[derive(Debug, Clone, PartialEq, Eq, Hash, Error)]  #[error("Invalid profession identifier: {0}")] @@ -88,7 +88,6 @@  //! While there are legitimate use cases for writing/modification support, they are currently not  //! implemented (but might be in a future version). -use std::collections::HashMap;  use std::convert::TryFrom;  use std::marker::PhantomData; @@ -105,9 +104,11 @@ mod processing;  pub use processing::{process, process_file, process_stream, Compression};  pub mod gamedata; -use gamedata::CmTrigger;  pub use gamedata::{Boss, EliteSpec, Profession}; +pub mod analyzers; +pub use analyzers::Analyzer; +  /// Any error that can occur during the processing of evtc files.  #[derive(Error, Debug)]  pub enum EvtcError { @@ -789,6 +790,11 @@ impl Log {          Boss::from_u16(self.boss_id)      } +    /// Return an analyzer suitable to analyze the given log. +    pub fn analyzer<'s>(&'s self) -> Option<Box<dyn Analyzer + 's>> { +        analyzers::for_log(&self) +    } +      /// Return all events present in this log.      #[inline]      pub fn events(&self) -> &[Event] { @@ -832,46 +838,7 @@ impl Log {      /// * We cannot determine whether the CM was active      /// * The boss is not known      pub fn is_cm(&self) -> bool { -        let trigger = self -            .encounter() -            .map(Boss::cm_trigger) -            .unwrap_or(CmTrigger::Unknown); -        match trigger { -            CmTrigger::HpThreshold(hp_threshold) => { -                for event in self.events() { -                    if let EventKind::MaxHealthUpdate { -                        agent_addr, -                        max_health, -                    } = *event.kind() -                    { -                        if self.is_boss(agent_addr) && max_health >= hp_threshold as u64 { -                            return true; -                        } -                    } -                } -                false -            } - -            CmTrigger::BuffPresent(wanted_buff_id) => { -                for event in self.events() { -                    if let EventKind::BuffApplication { buff_id, .. } = *event.kind() { -                        if buff_id == wanted_buff_id { -                            return true; -                        } -                    } -                } -                false -            } - -            CmTrigger::TimeBetweenBuffs(buff_id, threshold) => { -                let tbb = time_between_buffs(&self.events, buff_id); -                tbb != 0 && tbb <= threshold -            } - -            CmTrigger::Always => true, - -            CmTrigger::None | CmTrigger::Unknown => false, -        } +        self.analyzer().map(|a| a.is_cm()).unwrap_or(false)      }      /// Get the timestamp of when the log was started. @@ -925,35 +892,3 @@ impl Log {          })      }  } - -fn time_between_buffs(events: &[Event], wanted_buff_id: u32) -> u64 { -    let mut time_maps: HashMap<u64, Vec<u64>> = HashMap::new(); -    for event in events { -        if let EventKind::BuffApplication { -            destination_agent_addr, -            buff_id, -            .. -        } = event.kind() -        { -            if *buff_id == wanted_buff_id { -                time_maps -                    .entry(*destination_agent_addr) -                    .or_default() -                    .push(event.time()); -            } -        } -    } -    let timestamps = if let Some(ts) = time_maps.values().max_by_key(|v| v.len()) { -        ts -    } else { -        return 0; -    }; -    timestamps -        .iter() -        .zip(timestamps.iter().skip(1)) -        .map(|(a, b)| b - a) -        // Arbitrary limit to filter out duplicated buff application events -        .filter(|x| *x > 50) -        .min() -        .unwrap_or(0) -} | 
