aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDaniel Schadt <kingdread@gmx.de>2020-06-28 17:22:43 +0200
committerDaniel Schadt <kingdread@gmx.de>2020-06-28 17:22:43 +0200
commit0978345648cf9cdad6222f583dd21497b409d07e (patch)
tree9470f87a879e36c68104ef067c6657eb94cd98e5
parentacdc4d977e573d54c73530f77ba210efd2184cf0 (diff)
downloadevtclib-0978345648cf9cdad6222f583dd21497b409d07e.tar.gz
evtclib-0978345648cf9cdad6222f583dd21497b409d07e.tar.bz2
evtclib-0978345648cf9cdad6222f583dd21497b409d07e.zip
start implementing analyzers
It turns out that the different encounters do require quite some encounter-specific logic, not only to determine whether the CM was activated, but also to determine whether the fight was successful, the duration of the fight, later the phases, ... Wrapping all of this in pre-defined "triggers" (like CmTrigger) feels like it will be a bit unfitting, so with this patch we have introduced the evtclib::Analyzer, which can be used to analyze the fights. Currently, the whole CM detection logic has been moved to this new interface, and soon we also want the success-detection logic in there. The tests pass and the interface of Log::is_cm is unchanged.
-rw-r--r--src/analyzers/fractals.rs54
-rw-r--r--src/analyzers/helpers.rs70
-rw-r--r--src/analyzers/mod.rs65
-rw-r--r--src/analyzers/raids/mod.rs11
-rw-r--r--src/analyzers/raids/w4.rs116
-rw-r--r--src/analyzers/raids/w5.rs62
-rw-r--r--src/analyzers/raids/w6.rs87
-rw-r--r--src/analyzers/raids/w7.rs86
-rw-r--r--src/gamedata.rs59
-rw-r--r--src/lib.rs83
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}")]
diff --git a/src/lib.rs b/src/lib.rs
index 519d1c4..a0bb741 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -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)
-}