From d155a9cb179e3d2eff60ef4256790cfaf13546f0 Mon Sep 17 00:00:00 2001 From: Daniel Schadt Date: Thu, 31 Mar 2022 12:35:45 +0200 Subject: EoD strikes: implement Captain Mai Trin analyzer --- src/analyzers/mod.rs | 3 ++- src/analyzers/strikes.rs | 64 +++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 65 insertions(+), 2 deletions(-) diff --git a/src/analyzers/mod.rs b/src/analyzers/mod.rs index ed0f157..7b375af 100644 --- a/src/analyzers/mod.rs +++ b/src/analyzers/mod.rs @@ -132,9 +132,10 @@ pub fn for_log<'l>(log: &'l Log) -> Option> { | Encounter::FraenirOfJormag | Encounter::Boneskinner | Encounter::WhisperOfJormag - | Encounter::CaptainMaiTrin | Encounter::Ankka | Encounter::MinisterLi | Encounter::Dragonvoid => Some(Box::new(strikes::GenericStrike::new(log))), + + Encounter::CaptainMaiTrin => Some(Box::new(strikes::CaptainMaiTrin::new(log))), } } diff --git a/src/analyzers/strikes.rs b/src/analyzers/strikes.rs index 8c22c49..e12b8da 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,64 @@ 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; + pub const WINNING_BUFF: 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 { + 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::WINNING_BUFF + && *destination_agent_addr == mai.addr() + && event.time() > scarlet.first_aware() + { + return Some(Outcome::Success); + } + } + } + + Some(Outcome::Failure) + } +} -- cgit v1.2.3 From 5868e6c4924883700898c3a6805fdfe38de6aa85 Mon Sep 17 00:00:00 2001 From: Daniel Schadt Date: Thu, 31 Mar 2022 13:02:09 +0200 Subject: EoD strikes: implement Ankka analyzer --- src/analyzers/mod.rs | 2 +- src/analyzers/strikes.rs | 74 ++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 73 insertions(+), 3 deletions(-) diff --git a/src/analyzers/mod.rs b/src/analyzers/mod.rs index 7b375af..ba800a9 100644 --- a/src/analyzers/mod.rs +++ b/src/analyzers/mod.rs @@ -132,10 +132,10 @@ pub fn for_log<'l>(log: &'l Log) -> Option> { | Encounter::FraenirOfJormag | Encounter::Boneskinner | Encounter::WhisperOfJormag - | Encounter::Ankka | Encounter::MinisterLi | Encounter::Dragonvoid => 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))), } } diff --git a/src/analyzers/strikes.rs b/src/analyzers/strikes.rs index e12b8da..5c43865 100644 --- a/src/analyzers/strikes.rs +++ b/src/analyzers/strikes.rs @@ -38,6 +38,13 @@ impl<'log> Analyzer for GenericStrike<'log> { } } +/// The ID of the "Determined" buff. +/// +/// This buff is applied to the strike mission bosses when they die instead or phase. +/// +/// Teh chat link for this buff is `[&Bn8DAAA=]`. +pub const DETERMINED_ID: u32 = 895; + /// Analyzer for the Captain Mai Trin/Aetherblade Hideout strike. #[derive(Debug, Clone, Copy)] pub struct CaptainMaiTrin<'log> { @@ -46,7 +53,6 @@ pub struct CaptainMaiTrin<'log> { impl<'log> CaptainMaiTrin<'log> { pub const ECHO_OF_SCARLET_BRIAR: u16 = 24_768; - pub const WINNING_BUFF: u32 = 895; /// Create a new [`CaptainMaiTrin`] analyzer for the given log. /// @@ -86,7 +92,7 @@ impl<'log> Analyzer for CaptainMaiTrin<'log> { .. } = event.kind() { - if *buff_id == Self::WINNING_BUFF + if *buff_id == DETERMINED_ID && *destination_agent_addr == mai.addr() && event.time() > scarlet.first_aware() { @@ -98,3 +104,67 @@ impl<'log> Analyzer for CaptainMaiTrin<'log> { 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> { + /// 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 { + 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 == DETERMINED_ID + && *destination_agent_addr == ankka.addr() + && *duration == Self::DURATION_CUTOFF + } else { + false + } + }) + .count(); + + Outcome::from_bool(phase_change_count == Self::EXPECTED_PHASE_COUNT) + } +} -- cgit v1.2.3 From d79a214dd66a9cd2b5e631ba90178920891c8ee6 Mon Sep 17 00:00:00 2001 From: Daniel Schadt Date: Thu, 31 Mar 2022 13:16:00 +0200 Subject: EoD strikes: implement Minister Li analyzer --- src/analyzers/mod.rs | 2 +- src/analyzers/strikes.rs | 91 +++++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 83 insertions(+), 10 deletions(-) diff --git a/src/analyzers/mod.rs b/src/analyzers/mod.rs index ba800a9..8f9b1ff 100644 --- a/src/analyzers/mod.rs +++ b/src/analyzers/mod.rs @@ -132,10 +132,10 @@ pub fn for_log<'l>(log: &'l Log) -> Option> { | Encounter::FraenirOfJormag | Encounter::Boneskinner | Encounter::WhisperOfJormag - | Encounter::MinisterLi | Encounter::Dragonvoid => 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))), } } diff --git a/src/analyzers/strikes.rs b/src/analyzers/strikes.rs index 5c43865..4c43016 100644 --- a/src/analyzers/strikes.rs +++ b/src/analyzers/strikes.rs @@ -38,13 +38,6 @@ impl<'log> Analyzer for GenericStrike<'log> { } } -/// The ID of the "Determined" buff. -/// -/// This buff is applied to the strike mission bosses when they die instead or phase. -/// -/// Teh chat link for this buff is `[&Bn8DAAA=]`. -pub const DETERMINED_ID: u32 = 895; - /// Analyzer for the Captain Mai Trin/Aetherblade Hideout strike. #[derive(Debug, Clone, Copy)] pub struct CaptainMaiTrin<'log> { @@ -53,6 +46,13 @@ pub struct CaptainMaiTrin<'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. /// @@ -92,7 +92,7 @@ impl<'log> Analyzer for CaptainMaiTrin<'log> { .. } = event.kind() { - if *buff_id == DETERMINED_ID + if *buff_id == Self::DETERMINED_ID && *destination_agent_addr == mai.addr() && event.time() > scarlet.first_aware() { @@ -112,6 +112,13 @@ pub struct Ankka<'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. @@ -156,7 +163,7 @@ impl<'log> Analyzer for Ankka<'log> { .. } = event.kind() { - *buff_id == DETERMINED_ID + *buff_id == Self::DETERMINED_ID && *destination_agent_addr == ankka.addr() && *duration == Self::DURATION_CUTOFF } else { @@ -168,3 +175,69 @@ impl<'log> Analyzer for Ankka<'log> { 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 { + 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) + } +} -- cgit v1.2.3 From 10d88c9236cae5a3ed38f9ec9a87b97c14854ef2 Mon Sep 17 00:00:00 2001 From: Daniel Schadt Date: Thu, 31 Mar 2022 14:28:27 +0200 Subject: EoD strikes: implement Dragonvoid analyzer --- src/analyzers/mod.rs | 4 +-- src/analyzers/strikes.rs | 77 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+), 2 deletions(-) diff --git a/src/analyzers/mod.rs b/src/analyzers/mod.rs index 8f9b1ff..c27fefa 100644 --- a/src/analyzers/mod.rs +++ b/src/analyzers/mod.rs @@ -131,11 +131,11 @@ pub fn for_log<'l>(log: &'l Log) -> Option> { | Encounter::SuperKodanBrothers | Encounter::FraenirOfJormag | Encounter::Boneskinner - | Encounter::WhisperOfJormag - | 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 4c43016..9244124 100644 --- a/src/analyzers/strikes.rs +++ b/src/analyzers/strikes.rs @@ -241,3 +241,80 @@ impl<'log> Analyzer for MinisterLi<'log> { 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 { + // 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) + } +} -- cgit v1.2.3 From af780553850426b0f5e43ae08ac493b1c064153d Mon Sep 17 00:00:00 2001 From: Daniel Schadt Date: Thu, 31 Mar 2022 16:57:21 +0200 Subject: EoD strikes: add benchmarks for the analyzers --- benches/analyzers.rs | 5 +++++ 1 file changed, 5 insertions(+) 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"), } -- cgit v1.2.3 From e677ce0392368d7d7f78d0a78ad93128df27a205 Mon Sep 17 00:00:00 2001 From: Daniel Schadt Date: Thu, 31 Mar 2022 20:44:08 +0200 Subject: EoD strikes: also recognize other Dragonvoid IDs --- src/gamedata.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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), } } -- cgit v1.2.3 From 15c29b2435c48117b0e70a80ebbfbe37937af7cb Mon Sep 17 00:00:00 2001 From: Daniel Schadt Date: Fri, 1 Apr 2022 12:38:26 +0200 Subject: update changelog --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) 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 -- cgit v1.2.3