diff options
| -rw-r--r-- | CHANGELOG.md | 10 | ||||
| -rw-r--r-- | benches/analyzers.rs | 5 | ||||
| -rw-r--r-- | src/analyzers/mod.rs | 11 | ||||
| -rw-r--r-- | src/analyzers/strikes.rs | 284 | ||||
| -rw-r--r-- | src/gamedata.rs | 4 | 
5 files changed, 307 insertions, 7 deletions
| diff --git a/CHANGELOG.md b/CHANGELOG.md index 13357a1..8d4ac05 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,16 @@  All notable changes to this project will be documented in this file.  ## Unreleased +### Added +- Various analyzers for the End of Dragons strike missions: +  - `analyzers::strikes::CaptainMaiTrin` +  - `analyzers::strikes::Ankka` +  - `analyzers::strikes::MinisterLi` +  - `analyzers::strikes::Dragonvoid` + +### Fixed +- Success/failure detection for the End of Dragons strike missions. +- Some Dragonvoid logs not being recognized as such.  ## 0.7.0 - 2022-03-10  ### Added diff --git a/benches/analyzers.rs b/benches/analyzers.rs index ee25253..43a5a88 100644 --- a/benches/analyzers.rs +++ b/benches/analyzers.rs @@ -62,4 +62,9 @@ benchmarks! {      (fractal_skorvald, "skorvald", "tests/logs/skorvald-20200920.zevtc"),      (strike_generic, "generic-strike", "tests/logs/whisper-20200424.zevtc"), + +    (strike_mai_trin, "maitrin", "tests/logs/mai-trin-20220303.zevtc"), +    (strike_ankka, "ankka", "tests/logs/ankka-20220303.zevtc"), +    (strike_li, "li", "tests/logs/minister-li-20220303.zevtc"), +    (strike_dragonvoid, "dragonvoid", "tests/logs/dragonvoid-20220309.zevtc"),  } diff --git a/src/analyzers/mod.rs b/src/analyzers/mod.rs index ed0f157..c27fefa 100644 --- a/src/analyzers/mod.rs +++ b/src/analyzers/mod.rs @@ -131,10 +131,11 @@ pub fn for_log<'l>(log: &'l Log) -> Option<Box<dyn Analyzer + 'l>> {          | Encounter::SuperKodanBrothers          | Encounter::FraenirOfJormag          | Encounter::Boneskinner -        | Encounter::WhisperOfJormag -        | Encounter::CaptainMaiTrin -        | Encounter::Ankka -        | Encounter::MinisterLi -        | Encounter::Dragonvoid => Some(Box::new(strikes::GenericStrike::new(log))), +        | Encounter::WhisperOfJormag => Some(Box::new(strikes::GenericStrike::new(log))), + +        Encounter::CaptainMaiTrin => Some(Box::new(strikes::CaptainMaiTrin::new(log))), +        Encounter::Ankka => Some(Box::new(strikes::Ankka::new(log))), +        Encounter::MinisterLi => Some(Box::new(strikes::MinisterLi::new(log))), +        Encounter::Dragonvoid => Some(Box::new(strikes::Dragonvoid::new(log))),      }  } diff --git a/src/analyzers/strikes.rs b/src/analyzers/strikes.rs index 8c22c49..9244124 100644 --- a/src/analyzers/strikes.rs +++ b/src/analyzers/strikes.rs @@ -1,7 +1,8 @@  //! Analyzers for Strike Mission logs.  use crate::{      analyzers::{helpers, Analyzer, Outcome}, -    Log, +    gamedata::Boss, +    EventKind, Log,  };  /// Analyzer for strikes. @@ -36,3 +37,284 @@ impl<'log> Analyzer for GenericStrike<'log> {          Outcome::from_bool(helpers::boss_is_dead(self.log))      }  } + +/// Analyzer for the Captain Mai Trin/Aetherblade Hideout strike. +#[derive(Debug, Clone, Copy)] +pub struct CaptainMaiTrin<'log> { +    log: &'log Log, +} + +impl<'log> CaptainMaiTrin<'log> { +    pub const ECHO_OF_SCARLET_BRIAR: u16 = 24_768; +    /// Determined buff that is used in Mai Trin's Strike. +    /// +    /// Thanks to ArenaNet's consistency, there are multiple versions of the Determined buff in +    /// use. +    /// +    /// The chat link for this buff is `[&Bn8DAAA=]`. +    pub const DETERMINED_ID: u32 = 895; + +    /// Create a new [`CaptainMaiTrin`] 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 { +        CaptainMaiTrin { log } +    } +} + +impl<'log> Analyzer for CaptainMaiTrin<'log> { +    fn log(&self) -> &Log { +        self.log +    } + +    fn is_cm(&self) -> bool { +        // EoD strike CMs are not implemented yet as of 2022-03-31 +        false +    } + +    fn outcome(&self) -> Option<Outcome> { +        check_reward!(self.log); + +        let scarlet = self +            .log +            .characters() +            .find(|npc| npc.id() == Self::ECHO_OF_SCARLET_BRIAR)?; +        let mai = self +            .log +            .characters() +            .find(|npc| npc.id() == Boss::CaptainMaiTrin as u16)?; + +        for event in self.log.events() { +            if let EventKind::BuffApplication { +                destination_agent_addr, +                buff_id, +                .. +            } = event.kind() +            { +                if *buff_id == Self::DETERMINED_ID +                    && *destination_agent_addr == mai.addr() +                    && event.time() > scarlet.first_aware() +                { +                    return Some(Outcome::Success); +                } +            } +        } + +        Some(Outcome::Failure) +    } +} + +/// Analyzer for the Ankka/Xunlai Jade Junkyard strike. +#[derive(Debug, Clone, Copy)] +pub struct Ankka<'log> { +    log: &'log Log, +} + +impl<'log> Ankka<'log> { +    /// Determined buff that is used in Ankka's Strike. +    /// +    /// Thanks to ArenaNet's consistency, there are multiple versions of the Determined buff in +    /// use. +    /// +    /// The chat link for this buff is `[&Bn8DAAA=]`. +    pub const DETERMINED_ID: u32 = CaptainMaiTrin::DETERMINED_ID; +    /// The minimum duration of [`DETERMINED_ID`] buff applications. +    pub const DURATION_CUTOFF: i32 = i32::MAX; +    /// The expected number of times that Ankka needs to phase before we consider it a success. +    pub const EXPECTED_PHASE_COUNT: usize = 3; + +    /// Create a new [`Ankka`] 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 { +        Ankka { log } +    } +} + +impl<'log> Analyzer for Ankka<'log> { +    fn log(&self) -> &Log { +        self.log +    } + +    fn is_cm(&self) -> bool { +        // EoD strike CMs are not implemented yet as of 2022-03-31 +        false +    } + +    fn outcome(&self) -> Option<Outcome> { +        check_reward!(self.log); + +        let ankka = self +            .log +            .characters() +            .find(|npc| npc.id() == Boss::Ankka as u16)?; + +        let phase_change_count = self +            .log +            .events() +            .iter() +            .filter(|event| { +                if let EventKind::BuffApplication { +                    destination_agent_addr, +                    buff_id, +                    duration, +                    .. +                } = event.kind() +                { +                    *buff_id == Self::DETERMINED_ID +                        && *destination_agent_addr == ankka.addr() +                        && *duration == Self::DURATION_CUTOFF +                } else { +                    false +                } +            }) +            .count(); + +        Outcome::from_bool(phase_change_count == Self::EXPECTED_PHASE_COUNT) +    } +} + +/// Analyzer for the Minister Li/Kaineng Overlook strike. +#[derive(Debug, Clone, Copy)] +pub struct MinisterLi<'log> { +    log: &'log Log, +} + +impl<'log> MinisterLi<'log> { +    /// Determined buff that is used in Minister Li's Strike. +    /// +    /// Thanks to ArenaNet's consistency, there are multiple versions of the Determined buff in +    /// use. +    /// +    /// The chat link for this buff is `[&BvoCAAA=]`. +    pub const DETERMINED_ID: u32 = 762; +    /// The minimum number of times that Minister Li needs to phase before we consider it a success. +    pub const MINIMUM_PHASE_COUNT: usize = 3; + +    /// Create a new [`MinisterLi`] 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 { +        MinisterLi { log } +    } +} + +impl<'log> Analyzer for MinisterLi<'log> { +    fn log(&self) -> &Log { +        self.log +    } + +    fn is_cm(&self) -> bool { +        // EoD strike CMs are not implemented yet as of 2022-03-31 +        false +    } + +    fn outcome(&self) -> Option<Outcome> { +        check_reward!(self.log); + +        let li = self +            .log +            .characters() +            .find(|npc| npc.id() == Boss::MinisterLi as u16)?; + +        let phase_change_count = self +            .log +            .events() +            .iter() +            .filter(|event| { +                if let EventKind::BuffApplication { +                    destination_agent_addr, +                    buff_id, +                    .. +                } = event.kind() +                { +                    *buff_id == Self::DETERMINED_ID && *destination_agent_addr == li.addr() +                } else { +                    false +                } +            }) +            .count(); + +        Outcome::from_bool(phase_change_count >= Self::MINIMUM_PHASE_COUNT) +    } +} + +/// Analyzer for the Dragonvoid/Harvest Temple strike. +#[derive(Debug, Clone, Copy)] +pub struct Dragonvoid<'log> { +    log: &'log Log, +} + +impl<'log> Dragonvoid<'log> { +    pub const EXPECTED_TARGET_OFF_COUNT: usize = 2; + +    /// Create a new [`Dragonvoid`] 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 { +        Dragonvoid { log } +    } +} + +impl<'log> Analyzer for Dragonvoid<'log> { +    fn log(&self) -> &Log { +        self.log +    } + +    fn is_cm(&self) -> bool { +        // EoD strike CMs are not implemented yet as of 2022-03-31 +        false +    } + +    fn outcome(&self) -> Option<Outcome> { +        // check_reward is pointless because the reward is delayed. + +        // First, we find the right agent_addr +        let mut first_voids = None; +        for event in self.log.events() { +            if let EventKind::AttackTarget { +                agent_addr, +                parent_agent_addr, +                .. +            } = event.kind() +            { +                if first_voids.is_none() { +                    first_voids = Some(parent_agent_addr); +                } else if first_voids != Some(parent_agent_addr) { +                    // We find the amount of target off switches that occurred after a target on +                    // switch. +                    let mut is_on = false; +                    let mut target_off_count = 0; + +                    // The nested loop over events is not ideal, but it is currently the easiest +                    // way to implement this logic without trying to cram it into a single loop. +                    for e in self.log.events() { +                        if let EventKind::Targetable { +                            agent_addr: taa, +                            targetable, +                        } = e.kind() +                        { +                            if *taa != *agent_addr { +                                continue; +                            } +                            if *targetable { +                                is_on = true; +                            } else if !targetable && is_on { +                                target_off_count += 1; +                            } +                        } +                    } + +                    if target_off_count == Self::EXPECTED_TARGET_OFF_COUNT { +                        return Some(Outcome::Success); +                    } +                } +            } +        } +        Some(Outcome::Failure) +    } +} diff --git a/src/gamedata.rs b/src/gamedata.rs index c607147..f880ba6 100644 --- a/src/gamedata.rs +++ b/src/gamedata.rs @@ -48,6 +48,8 @@ impl FromStr for GameMode {      }  } +static DRAGONVOID_IDS: &[u16] = &[Encounter::Dragonvoid as u16, 0xA9E0, 0x5F37]; +  /// Enum containing all encounters with their IDs.  ///  /// An encounter is a fight or event for which a log can exist. An encounter consists of no, one or @@ -229,7 +231,7 @@ impl Encounter {          match id {              _ if id == Encounter::TwistedCastle as u16 => Some(Encounter::TwistedCastle),              _ if id == Encounter::RiverOfSouls as u16 => Some(Encounter::RiverOfSouls), -            _ if id == Encounter::Dragonvoid as u16 => Some(Encounter::Dragonvoid), +            _ if DRAGONVOID_IDS.contains(&id) => Some(Encounter::Dragonvoid),              _ => Boss::from_u16(id).map(Boss::encounter),          }      } | 
