diff options
| author | Daniel Schadt <kingdread@gmx.de> | 2020-07-24 14:23:53 +0200 | 
|---|---|---|
| committer | Daniel Schadt <kingdread@gmx.de> | 2020-07-24 14:23:53 +0200 | 
| commit | 71528905ed228750559a41144a2e0a95db3e6805 (patch) | |
| tree | 4e46c6cbd3a3e83ab707e7156b345fbe7f3048ea /src/analyzers/raids | |
| parent | 01354b0934409c355831bb4202f998fe5dbdc335 (diff) | |
| parent | 9d27ec7034f9ad07d8a1d74ab30fdc470de4e02d (diff) | |
| download | evtclib-71528905ed228750559a41144a2e0a95db3e6805.tar.gz evtclib-71528905ed228750559a41144a2e0a95db3e6805.tar.bz2 evtclib-71528905ed228750559a41144a2e0a95db3e6805.zip  | |
Merge branch 'analyzers'
This brings in proper fight outcome detection, which is nice and needed
for downstream applications (raidgrep/ezau).
Furthermore, this cleans up the CM detection a bit by moving away from
the "descriptive" trigger way to just having dynamically dispatched
methods for every log.
Diffstat (limited to 'src/analyzers/raids')
| -rw-r--r-- | src/analyzers/raids/mod.rs | 60 | ||||
| -rw-r--r-- | src/analyzers/raids/w3.rs | 34 | ||||
| -rw-r--r-- | src/analyzers/raids/w4.rs | 230 | ||||
| -rw-r--r-- | src/analyzers/raids/w5.rs | 90 | ||||
| -rw-r--r-- | src/analyzers/raids/w6.rs | 149 | ||||
| -rw-r--r-- | src/analyzers/raids/w7.rs | 111 | 
6 files changed, 674 insertions, 0 deletions
diff --git a/src/analyzers/raids/mod.rs b/src/analyzers/raids/mod.rs new file mode 100644 index 0000000..bb3824b --- /dev/null +++ b/src/analyzers/raids/mod.rs @@ -0,0 +1,60 @@ +//! Analyzers for raid logs. +//! +//! Most of the fights can use the [`GenericRaid`][GenericRaid] analyzer. The exception to this are +//! fights which have a Challenge Mote (Wing 4, Wing 5, Wing 6, Wing 7), and fights which need to +//! use a different method to determine their outcome (Xera, Deimos, Soulless Horror, Conjured +//! Amalgamate, Qadim). +use crate::{ +    analyzers::{helpers, Analyzer, Outcome}, +    Log, +}; + +mod w3; +pub use w3::Xera; + +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}; + +/// A generic raid analyzer that works for bosses without special interactions. +/// +/// This analyzer always returns `false` for the Challenge Mote calculation. +/// +/// The outcome of the fight is determined by whether the boss agent has a death event - which +/// works for a lot of fights, but not all of them. +#[derive(Debug, Clone, Copy)] +pub struct GenericRaid<'log> { +    log: &'log Log, +} + +impl<'log> GenericRaid<'log> { +    /// Create a new [`GenericRaid`] analyzer for the given log. +    /// +    /// **Do not** use this method unless you know what you are doing. Instead, rely on +    /// [`Log::analyzer`]! +    pub fn new(log: &'log Log) -> Self { +        GenericRaid { log } +    } +} + +impl<'log> Analyzer for GenericRaid<'log> { +    fn log(&self) -> &Log { +        self.log +    } + +    fn is_cm(&self) -> bool { +        false +    } + +    fn outcome(&self) -> Option<Outcome> { +        Outcome::from_bool(helpers::boss_is_dead(self.log)) +    } +} diff --git a/src/analyzers/raids/w3.rs b/src/analyzers/raids/w3.rs new file mode 100644 index 0000000..1b80b8d --- /dev/null +++ b/src/analyzers/raids/w3.rs @@ -0,0 +1,34 @@ +use crate::{ +    analyzers::{helpers, Analyzer, Outcome}, +    Log, +}; + +/// Analyzer for the final fight of Wing 3, Xera. +#[derive(Debug, Clone, Copy)] +pub struct Xera<'log> { +    log: &'log Log, +} + +impl<'log> Xera<'log> { +    /// Create a new [`Xera`] analyzer for the given log. +    /// +    /// **Do not** use this method unless you know what you are doing. Instead, rely on +    /// [`Log::analyzer`]! +    pub fn new(log: &'log Log) -> Self { +        Xera { log } +    } +} + +impl<'log> Analyzer for Xera<'log> { +    fn log(&self) -> &Log { +        self.log +    } + +    fn is_cm(&self) -> bool { +        false +    } + +    fn outcome(&self) -> Option<Outcome> { +        Outcome::from_bool(helpers::players_exit_after_boss(self.log)) +    } +} diff --git a/src/analyzers/raids/w4.rs b/src/analyzers/raids/w4.rs new file mode 100644 index 0000000..310b26f --- /dev/null +++ b/src/analyzers/raids/w4.rs @@ -0,0 +1,230 @@ +//! Boss fight analyzers for Wing 4 (Bastion of the Penitent). +use crate::{ +    analyzers::{helpers, Analyzer, Outcome}, +    EventKind, 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> { +    /// Create a new [`Cairn`] analyzer for the given log. +    /// +    /// **Do not** use this method unless you know what you are doing. Instead, rely on +    /// [`Log::analyzer`]! +    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) +    } + +    fn outcome(&self) -> Option<Outcome> { +        Outcome::from_bool(helpers::boss_is_dead(self.log)) +    } +} + +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> { +    /// Create a new [`MursaatOverseer`] analyzer for the given log. +    /// +    /// **Do not** use this method unless you know what you are doing. Instead, rely on +    /// [`Log::analyzer`]! +    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) +    } + +    fn outcome(&self) -> Option<Outcome> { +        Outcome::from_bool(helpers::boss_is_dead(self.log)) +    } +} + +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> { +    /// Create a new [`Samarog`] analyzer for the given log. +    /// +    /// **Do not** use this method unless you know what you are doing. Instead, rely on +    /// [`Log::analyzer`]! +    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) +    } + +    fn outcome(&self) -> Option<Outcome> { +        Outcome::from_bool(helpers::boss_is_dead(self.log)) +    } +} + +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> { +    /// Create a new [`Deimos`] analyzer for the given log. +    /// +    /// **Do not** use this method unless you know what you are doing. Instead, rely on +    /// [`Log::analyzer`]! +    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) +    } + +    fn outcome(&self) -> Option<Outcome> { +        // The idea for Deimos is that we first need to figure out when the 10% split happens (if +        // it even happens), then we can find the time when 10%-Deimos becomes untargetable and +        // then we can compare this time to the player exit time. + +        let split_time = deimos_10_time(self.log); +        // We never got to 10%, so this is a fail. +        if split_time == 0 { +            return Some(Outcome::Failure); +        } + +        let at_address = deimos_at_address(self.log); +        if at_address == 0 { +            return Some(Outcome::Failure); +        } + +        let mut player_exit = 0u64; +        let mut at_exit = 0u64; +        for event in self.log.events() { +            match event.kind() { +                EventKind::ExitCombat { agent_addr } +                    if self +                        .log +                        .agent_by_addr(*agent_addr) +                        .map(|a| a.kind().is_player()) +                        .unwrap_or(false) +                        && event.time() >= player_exit => +                { +                    player_exit = event.time(); +                } + +                EventKind::Targetable { +                    agent_addr, +                    targetable, +                } if *agent_addr == at_address && !targetable && event.time() >= at_exit => { +                    at_exit = event.time(); +                } + +                _ => (), +            } +        } + +        // Safety margin +        Outcome::from_bool(player_exit > at_exit + 1000) +    } +} + +// Extracts the timestamp when Deimos's 10% phase started. +// +// This function may panic when passed non-Deimos logs! +fn deimos_10_time(log: &Log) -> u64 { +    let mut first_aware = 0u64; + +    for event in log.events() { +        if let EventKind::Targetable { targetable, .. } = event.kind() { +            if *targetable { +                first_aware = event.time(); +                println!("First aware: {}", first_aware); +            } +        } +    } + +    first_aware +} + +// Returns the attack target address for the 10% Deimos phase. +// +// Returns 0 when the right attack target is not found. +fn deimos_at_address(log: &Log) -> u64 { +    for event in log.events().iter().rev() { +        if let EventKind::AttackTarget { +            agent_addr, +            parent_agent_addr, +            .. +        } = event.kind() +        { +            let parent = log.agent_by_addr(*parent_agent_addr); +            if let Some(parent) = parent { +                if Some("Deimos") == parent.as_gadget().map(|g| g.name()) { +                    return *agent_addr; +                } +            } +        } +    } +    0 +} diff --git a/src/analyzers/raids/w5.rs b/src/analyzers/raids/w5.rs new file mode 100644 index 0000000..578cea8 --- /dev/null +++ b/src/analyzers/raids/w5.rs @@ -0,0 +1,90 @@ +//! Boss fight analyzers for Wing 5 (Hall of Chains) +use crate::{ +    analyzers::{helpers, Analyzer, Outcome}, +    EventKind, Log, +}; + +pub const DESMINA_BUFF_ID: u32 = 47414; +pub const DESMINA_MS_THRESHOLD: u64 = 11_000; +pub const DESMINA_DEATH_BUFF: u32 = 895; + +/// 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> { +    /// Create a new [`SoullessHorror`] analyzer for the given log. +    /// +    /// **Do not** use this method unless you know what you are doing. Instead, rely on +    /// [`Log::analyzer`]! +    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 +    } + +    fn outcome(&self) -> Option<Outcome> { +        Outcome::from_bool(self.log.events().iter().any(|event| { +            if let EventKind::BuffApplication { +                buff_id, +                destination_agent_addr, +                .. +            } = event.kind() +            { +                self.log.is_boss(*destination_agent_addr) && *buff_id == DESMINA_DEATH_BUFF +            } else { +                false +            } +        })) +    } +} + +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> { +    /// Create a new [`Dhuum`] analyzer for the given log. +    /// +    /// **Do not** use this method unless you know what you are doing. Instead, rely on +    /// [`Log::analyzer`]! +    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) +    } + +    fn outcome(&self) -> Option<Outcome> { +        Outcome::from_bool(helpers::boss_is_dead(self.log)) +    } +} diff --git a/src/analyzers/raids/w6.rs b/src/analyzers/raids/w6.rs new file mode 100644 index 0000000..8701a63 --- /dev/null +++ b/src/analyzers/raids/w6.rs @@ -0,0 +1,149 @@ +//! Boss fight analyzers for Wing 6 (Mythwright Gambit) +use crate::{ +    analyzers::{helpers, Analyzer, Outcome}, +    gamedata::{KENUT_ID, NIKARE_ID}, +    EventKind, Log, +}; + +pub const CA_CM_BUFF: u32 = 53_075; +pub const ZOMMOROS_ID: u16 = 21_118; + +/// 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> { +    /// Create a new [`ConjuredAmalgamate`] analyzer for the given log. +    /// +    /// **Do not** use this method unless you know what you are doing. Instead, rely on +    /// [`Log::analyzer`]! +    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) +    } + +    fn outcome(&self) -> Option<Outcome> { +        for event in self.log.events() { +            if let EventKind::Spawn { agent_addr } = event.kind() { +                if self +                    .log +                    .agent_by_addr(*agent_addr) +                    .and_then(|a| a.as_character()) +                    .map(|a| a.id() == ZOMMOROS_ID) +                    .unwrap_or(false) +                { +                    return Some(Outcome::Success); +                } +            } +        } +        Some(Outcome::Failure) +    } +} + +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> { +    /// Create a new [`LargosTwins`] analyzer for the given log. +    /// +    /// **Do not** use this method unless you know what you are doing. Instead, rely on +    /// [`Log::analyzer`]! +    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) +    } + +    fn outcome(&self) -> Option<Outcome> { +        let mut nikare_dead = false; +        let mut kenut_dead = false; + +        for event in self.log.events() { +            if let EventKind::ChangeDead { agent_addr } = event.kind() { +                let agent = if let Some(agent) = self +                    .log +                    .agent_by_addr(*agent_addr) +                    .and_then(|a| a.as_character()) +                { +                    agent +                } else { +                    continue; +                }; + +                if agent.id() == NIKARE_ID { +                    nikare_dead = true; +                } else if agent.id() == KENUT_ID { +                    kenut_dead = true; +                } +            } +        } + +        Outcome::from_bool(kenut_dead && nikare_dead) +    } +} + +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> { +    /// Create a new [`Qadim`] analyzer for the given log. +    /// +    /// **Do not** use this method unless you know what you are doing. Instead, rely on +    /// [`Log::analyzer`]! +    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) +    } + +    fn outcome(&self) -> Option<Outcome> { +        Outcome::from_bool(helpers::players_exit_after_boss(self.log)) +    } +} diff --git a/src/analyzers/raids/w7.rs b/src/analyzers/raids/w7.rs new file mode 100644 index 0000000..bdfadd6 --- /dev/null +++ b/src/analyzers/raids/w7.rs @@ -0,0 +1,111 @@ +//! Boss fight analyzers for Wing 6 (Mythwright Gambit) +use crate::{ +    analyzers::{helpers, Analyzer, Outcome}, +    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> { +    /// Create a new [`CardinalAdina`] analyzer for the given log. +    /// +    /// **Do not** use this method unless you know what you are doing. Instead, rely on +    /// [`Log::analyzer`]! +    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) +    } + +    fn outcome(&self) -> Option<Outcome> { +        Outcome::from_bool(helpers::boss_is_dead(self.log)) +    } +} + +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> { +    /// Create a new [`CardinalSabir`] analyzer for the given log. +    /// +    /// **Do not** use this method unless you know what you are doing. Instead, rely on +    /// [`Log::analyzer`]! +    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) +    } + +    fn outcome(&self) -> Option<Outcome> { +        Outcome::from_bool(helpers::boss_is_dead(self.log)) +    } +} + +pub const QADIMP_CM_HEALTH: u64 = 51_000_000; + +/// Analyzer for the final fight of Wing 7, Qadim The Peerless. +#[derive(Debug, Clone, Copy)] +pub struct QadimThePeerless<'log> { +    log: &'log Log, +} + +impl<'log> QadimThePeerless<'log> { +    /// Create a new [`QadimThePeerless`] analyzer for the given log. +    /// +    /// **Do not** use this method unless you know what you are doing. Instead, rely on +    /// [`Log::analyzer`]! +    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) +    } + +    fn outcome(&self) -> Option<Outcome> { +        Outcome::from_bool(helpers::boss_is_dead(self.log)) +    } +}  | 
