diff options
-rw-r--r-- | src/gamedata.rs | 149 | ||||
-rw-r--r-- | src/lib.rs | 5 | ||||
-rw-r--r-- | src/statistics/boon.rs | 361 | ||||
-rw-r--r-- | src/statistics/damage.rs | 95 | ||||
-rw-r--r-- | src/statistics/gamedata.rs | 294 | ||||
-rw-r--r-- | src/statistics/math.rs | 240 | ||||
-rw-r--r-- | src/statistics/mechanics.rs | 62 | ||||
-rw-r--r-- | src/statistics/mod.rs | 145 | ||||
-rw-r--r-- | src/statistics/trackers.rs | 443 |
9 files changed, 151 insertions, 1643 deletions
diff --git a/src/gamedata.rs b/src/gamedata.rs new file mode 100644 index 0000000..c56af13 --- /dev/null +++ b/src/gamedata.rs @@ -0,0 +1,149 @@ +//! This module contains some low-level game data, such as different boss IDs. +use std::{fmt, str::FromStr}; +use num_derive::FromPrimitive; + +/// Enum containing all bosses with their IDs. +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, FromPrimitive)] +pub enum Boss { + // Wing 1 + ValeGuardian = 0x3C4E, + Gorseval = 0x3C45, + Sabetha = 0x3C0F, + + // Wing 2 + Slothasor = 0x3EFB, + Matthias = 0x3EF3, + + // Wing 3 + KeepConstruct = 0x3F6B, + /// Xera ID for phase 1. + /// + /// This is only half of Xera's ID, as there will be a second agent for the + /// second phase. This agent will have another ID, see + /// [`XERA_PHASE2_ID`](constant.XERA_PHASE2_ID.html). + Xera = 0x3F76, + + // Wing 4 + Cairn = 0x432A, + MursaatOverseer = 0x4314, + Samarog = 0x4324, + Deimos = 0x4302, + + // Wing 5 + SoullessHorror = 0x4D37, + Dhuum = 0x4BFA, + + // Wing 6 + ConjuredAmalgamate = 0xABC6, + LargosTwins = 0x5271, + Qadim = 0x51C6, + + // Wing 7 + CardinalAdina = 0x55F6, + CardinalSabir = 0x55CC, + QadimThePeerless = 0x55F0, + + // 100 CM + Skorvald = 0x44E0, + Artsariiv = 0x461D, + Arkk = 0x455F, + + // 99 CM + MAMA = 0x427D, + Siax = 0x4284, + Ensolyss = 0x4234, + + // Strike missions + IcebroodConstruct = 0x568A, + VoiceOfTheFallen = 0x5747, + FraenirOfJormag = 0x57DC, + Boneskinner = 0x57F9, + WhisperOfJormag = 0x58B7, +} + + +/// Error for when converting a string to the boss fails. +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +pub struct ParseBossError(String); + + +impl fmt::Display for ParseBossError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "Invalid boss identifier: {}", self.0) + } +} + + +impl FromStr for Boss { + type Err = ParseBossError; + + fn from_str(s: &str) -> Result<Self, Self::Err> { + let lower = s.to_lowercase(); + match &lower as &str { + "vg" | "vale guardian" => Ok(Boss::ValeGuardian), + "gorse" | "gorseval" => Ok(Boss::Gorseval), + "sab" | "sabetha" => Ok(Boss::Sabetha), + + "sloth" | "slothasor" => Ok(Boss::Slothasor), + "matthias" => Ok(Boss::Matthias), + + "kc" | "keep construct" => Ok(Boss::KeepConstruct), + "xera" => Ok(Boss::Xera), + + "cairn" => Ok(Boss::Cairn), + "mo" | "mursaat overseer" => Ok(Boss::MursaatOverseer), + "sam" | "sama" | "samarog" => Ok(Boss::Samarog), + "deimos" => Ok(Boss::Deimos), + + "desmina" | "sh" => Ok(Boss::SoullessHorror), + "dhuum" => Ok(Boss::Dhuum), + + "ca" | "conjured almagamate" => Ok(Boss::ConjuredAmalgamate), + "largos" | "twins" => Ok(Boss::LargosTwins), + "qadim" => Ok(Boss::Qadim), + + "adina" | "cardinal adina" => Ok(Boss::CardinalAdina), + "sabir" | "cardinal sabir" => Ok(Boss::CardinalSabir), + "qadimp" | "peerless qadim" | "qadim the peerless" => Ok(Boss::QadimThePeerless), + + "skorvald" => Ok(Boss::Skorvald), + "artsariiv" => Ok(Boss::Artsariiv), + "arkk" => Ok(Boss::Arkk), + + "mama" => Ok(Boss::MAMA), + "siax" => Ok(Boss::Siax), + "ensolyss" => Ok(Boss::Ensolyss), + + "icebrood" | "icebrood construct" => Ok(Boss::IcebroodConstruct), + "super kodan brothers" => Ok(Boss::VoiceOfTheFallen), + "fraenir" | "fraenir of jormag" => Ok(Boss::FraenirOfJormag), + "boneskinner" => Ok(Boss::Boneskinner), + "whisper" | "whisper of jormag" => Ok(Boss::WhisperOfJormag), + + _ => Err(ParseBossError(s.to_owned())) + } + } +} + + +/// ID for Xera in the second phase. +/// +/// The original Xera will despawn, and after the tower phase, a separate spawn +/// will take over. This new Xera will have this ID. Care needs to be taken when +/// calculating boss damage on this encounter, as both Xeras have to be taken +/// into account. +pub const XERA_PHASE2_ID: u16 = 0x3F9E; + + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + pub fn test_parsing() { + assert_eq!("vg".parse(), Ok(Boss::ValeGuardian)); + assert_eq!("VG".parse(), Ok(Boss::ValeGuardian)); + + assert!("vga".parse::<Boss>().is_err()); + } +} @@ -26,9 +26,8 @@ pub mod raw; mod event; pub use event::{Event, EventKind}; -pub mod statistics; - -use statistics::gamedata::{self, Boss}; +pub mod gamedata; +pub use gamedata::Boss; use std::fmt; diff --git a/src/statistics/boon.rs b/src/statistics/boon.rs deleted file mode 100644 index 196bd1d..0000000 --- a/src/statistics/boon.rs +++ /dev/null @@ -1,361 +0,0 @@ -//! Module providing functions and structs to deal with boon related statistics. -use std::cmp; -use std::fmt; -use std::ops::Mul; - -use super::math::{Monoid, RecordFunc, Semigroup}; - -use fnv::FnvHashMap; - -/// The type of a boon. -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -pub enum BoonType { - /// Boon stacks duration, e.g. Regeneration. - Duration, - /// Boon stacks intensity, e.g. Might. - Intensity, -} - -/// A struct that helps with simulating boon changes over time. -/// -/// This basically simulates a single boon-queue (for a single boon). -/// -/// # A quick word about how boon queues work -/// -/// For each boon, you have an internal *boon queue*, limited to a specific -/// capacity. When the current stack expires, the next one is taken from the -/// queue. -/// -/// The queue is sorted by boon strength. This means that "weak" boons are -/// always at the end (and as such, are the first ones to be deleted when the -/// queue is full). This prevents "bad" boons (e.g. the Quickness from Lightning -/// Hammer #2) to override the "good" boons (e.g. the Quickness from your -/// friendly neighborhood Chrono with 100% boon duration). -/// -/// This also means that boons can be "lost". If the queue is full, the boon -/// might not get applied, or it might replace another boon, thus wasting some -/// of the boon duration. -/// -/// Intensity-stacked boons (such as Might) work a bit differently: as time -/// passes, all stacks are decreased simultaneously! As soon as a stack reaches -/// 0, it is dropped. -/// -/// You can find more information and the size of some of the queues on the wiki: -/// https://wiki.guildwars2.com/wiki/Effect_stacking -#[derive(Clone, Debug)] -pub struct BoonQueue { - capacity: u32, - queue: Vec<u64>, - boon_type: BoonType, - next_update: u64, - backlog: u64, -} - -impl BoonQueue { - /// Create a new boon queue. - /// - /// * `capacity` - The capacity of the queue. - /// * `boon_type` - How the boons stack. - pub fn new(capacity: u32, boon_type: BoonType) -> BoonQueue { - BoonQueue { - capacity, - queue: Vec::new(), - boon_type, - next_update: 0, - backlog: 0, - } - } - - fn fix_queue(&mut self) { - // Sort reversed, so that the longest stack is at the front. - self.queue.sort_unstable_by(|a, b| b.cmp(a)); - // Truncate queue by cutting of the shortest stacks - if self.queue.len() > self.capacity as usize { - self.queue.drain(self.capacity as usize..); - } - } - - /// Get the type of this boon. - pub fn boon_type(&self) -> BoonType { - self.boon_type - } - - /// Add a boon stack to this queue. - /// - /// * `duration` - Duration (in milliseconds) of the added stack. - pub fn add_stack(&mut self, duration: u64) { - let backlog = self.backlog; - self.do_simulate(backlog); - self.queue.push(duration); - self.fix_queue(); - self.next_update = self.next_update(); - } - - /// Return the amount of current stacks. - /// - /// If the boon type is a duration boon, this will always return 0 or 1. - /// - /// If the boon type is an intensity boon, it will return the number of - /// stacks. - pub fn current_stacks(&self) -> u32 { - let result = match self.boon_type { - BoonType::Intensity => self.queue.len(), - BoonType::Duration => cmp::min(1, self.queue.len()), - }; - result as u32 - } - - /// Simulate time passing. - /// - /// This will decrease the remaining duration of the stacks accordingly. - /// - /// * `duration` - The amount of time (in milliseconds) to simulate. - pub fn simulate(&mut self, duration: u64) { - if duration == 0 { - return; - } - if duration < self.next_update { - self.next_update -= duration; - self.backlog += duration; - } else { - let total = self.backlog + duration; - self.do_simulate(total); - } - } - - /// Simulate the thing without using the backlog. - fn do_simulate(&mut self, duration: u64) { - let mut remaining = duration; - match self.boon_type { - BoonType::Duration => { - while remaining > 0 && !self.queue.is_empty() { - let next = self.queue.remove(0); - if next > remaining { - self.queue.push(next - remaining); - break; - } else { - remaining -= next; - } - } - self.fix_queue(); - } - - BoonType::Intensity => { - self.queue = self - .queue - .iter() - .cloned() - .filter(|v| *v > duration) - .map(|v| v - duration) - .collect(); - } - } - self.next_update = self.next_update(); - self.backlog = 0; - } - - /// Remove all stacks. - pub fn clear(&mut self) { - self.queue.clear(); - self.next_update = 0; - self.backlog = 0; - } - - /// Cleanse a single stack - pub fn drop_single(&mut self) { - if self.is_empty() { - return; - } - self.queue.pop(); - } - - /// Checks if any stacks are left. - pub fn is_empty(&self) -> bool { - self.queue.is_empty() - } - - /// Calculate when the stacks will have the next visible change. - /// - /// This assumes that the stacks will not be modified during this time. - /// - /// The return value is the duration in milliseconds. If the boon queue is - /// currently empty, 0 is returned. - pub fn next_change(&self) -> u64 { - match self.boon_type { - BoonType::Duration => self.queue.iter().sum(), - BoonType::Intensity => self.queue.last().cloned().unwrap_or(0), - } - } - - /// Calculate when the boon queue should be updated next. - /// - /// The next update always means that a stack runs out, even if it has no - /// visible effect. - /// - /// For each queue: `next_update() <= next_change()`. - /// - /// A return value of 0 means that there's no update awaiting. - pub fn next_update(&self) -> u64 { - self.queue.last().cloned().unwrap_or(0) - } -} - -/// Amount of stacks of a boon. -// Since this is also used to represent changes in stacks, we need access to -// negative numbers too, as stacks can drop. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub struct Stacks(i32); - -impl Semigroup for Stacks { - #[inline] - fn combine(&self, other: &Self) -> Self { - Stacks(self.0 + other.0) - } -} - -impl Monoid for Stacks { - #[inline] - fn mempty() -> Self { - Stacks(0) - } -} - -// This shouldn't be negative, as total stacks are always greater than 0, thus -// the area below the curve will always be positive. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -#[doc(hidden)] -pub struct BoonArea(u64); - -impl Semigroup for BoonArea { - #[inline] - fn combine(&self, other: &Self) -> Self { - BoonArea(self.0 + other.0) - } -} - -impl Monoid for BoonArea { - #[inline] - fn mempty() -> Self { - BoonArea(0) - } -} - -impl Mul<u64> for Stacks { - type Output = BoonArea; - - #[inline] - fn mul(self, rhs: u64) -> BoonArea { - BoonArea(self.0 as u64 * rhs) - } -} - -/// A boon log for a specific player. -/// -/// This logs the amount of stacks of each boon a player had at any given time. -#[derive(Clone, Default)] -pub struct BoonLog { - // Keep a separate RecordFunc for each boon. - inner: FnvHashMap<u32, RecordFunc<u64, (), Stacks>>, -} - -impl BoonLog { - /// Create a new, empty boon log. - pub fn new() -> Self { - Default::default() - } - - /// Add an event to the boon log. - pub fn log(&mut self, time: u64, boon_id: u32, stacks: u32) { - let func = self.inner.entry(boon_id).or_insert_with(Default::default); - let current = func.tally(); - if current.0 == stacks as i32 { - return; - } - let diff = stacks as i32 - current.0; - func.insert(time, (), Stacks(diff)); - } - - /// Get the average amount of stacks between the two given time points. - /// - /// * `a` - Start time point. - /// * `b` - End time point. - /// * `boon_id` - ID of the boon that you want to get the average for. - pub fn average_stacks(&self, a: u64, b: u64, boon_id: u32) -> f32 { - assert!(b >= a, "timespan is negative?!"); - let func = if let Some(f) = self.inner.get(&boon_id) { - f - } else { - return 0.; - }; - let area = func.integral(&a, &b); - area.0 as f32 / (b - a) as f32 - } - - /// Get the amount of stacks at the given time point. - /// - /// * `x` - Time point. - /// * `boon_id` - ID of the boon that you want to get. - pub fn stacks_at(&self, x: u64, boon_id: u32) -> u32 { - self.inner - .get(&boon_id) - .map(|f| f.get(&x)) - .unwrap_or(Stacks(0)) - .0 as u32 - } -} - -impl fmt::Debug for BoonLog { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "BoonLog {{ .. }}") - } -} - -#[cfg(test)] -mod test { - use super::*; - - #[test] - fn test_queue_capacity() { - let mut queue = BoonQueue::new(5, BoonType::Intensity); - assert_eq!(queue.current_stacks(), 0); - for _ in 0..10 { - queue.add_stack(10); - } - assert_eq!(queue.current_stacks(), 5); - } - - #[test] - fn test_simulate_duration() { - let mut queue = BoonQueue::new(10, BoonType::Duration); - queue.add_stack(10); - queue.add_stack(20); - assert_eq!(queue.current_stacks(), 1); - queue.simulate(30); - assert_eq!(queue.current_stacks(), 0); - - queue.add_stack(50); - queue.simulate(30); - assert_eq!(queue.current_stacks(), 1); - queue.simulate(10); - assert_eq!(queue.current_stacks(), 1); - queue.simulate(15); - assert_eq!(queue.current_stacks(), 0); - } - - #[test] - fn test_simulate_intensity() { - let mut queue = BoonQueue::new(5, BoonType::Intensity); - - queue.add_stack(10); - queue.add_stack(20); - assert_eq!(queue.current_stacks(), 2); - - queue.simulate(5); - assert_eq!(queue.current_stacks(), 2); - - queue.simulate(5); - assert_eq!(queue.current_stacks(), 1); - queue.simulate(15); - assert_eq!(queue.current_stacks(), 0); - } -} diff --git a/src/statistics/damage.rs b/src/statistics/damage.rs deleted file mode 100644 index 0c26a9b..0000000 --- a/src/statistics/damage.rs +++ /dev/null @@ -1,95 +0,0 @@ -use super::math::{Monoid, RecordFunc, Semigroup}; -use std::fmt; - -/// Type of the damage. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum DamageType { - Physical, - Condition, -} - -/// Meta information about a damage log entry. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub struct Meta { - pub source: u64, - pub target: u64, - pub kind: DamageType, - pub skill: u32, -} - -/// A small wrapper that wraps a damage number. -#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] -pub struct Damage(pub u64); - -impl Semigroup for Damage { - #[inline] - fn combine(&self, other: &Self) -> Self { - Damage(self.0 + other.0) - } -} - -impl Monoid for Damage { - #[inline] - fn mempty() -> Self { - Damage(0) - } -} - -/// Provides access to the damage log. -#[derive(Clone, Default)] -pub struct DamageLog { - inner: RecordFunc<u64, Meta, Damage>, -} - -impl DamageLog { - pub fn new() -> Self { - DamageLog { - inner: RecordFunc::new(), - } - } - - pub fn log( - &mut self, - time: u64, - source: u64, - target: u64, - kind: DamageType, - skill: u32, - value: u64, - ) { - self.inner.insert( - time, - Meta { - source, - target, - kind, - skill, - }, - Damage(value), - ) - } - - pub fn damage_between<F: FnMut(&Meta) -> bool>( - &self, - start: u64, - stop: u64, - filter: F, - ) -> Damage { - self.inner.between_only(&start, &stop, filter) - } - - pub fn damage<F: FnMut(&Meta) -> bool>(&self, filter: F) -> Damage { - self.inner.tally_only(filter) - } -} - -impl fmt::Debug for DamageLog { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!( - f, - "DamageLog {{ {} events logged, {:?} total damage }}", - self.inner.len(), - self.inner.tally() - ) - } -} diff --git a/src/statistics/gamedata.rs b/src/statistics/gamedata.rs deleted file mode 100644 index d942939..0000000 --- a/src/statistics/gamedata.rs +++ /dev/null @@ -1,294 +0,0 @@ -//! This module contains some game data that is necessary to correctly calculate -//! some statistics. -use std::{fmt, str::FromStr}; -use super::boon::{BoonQueue, BoonType}; -use num_derive::FromPrimitive; - -/// Enum containing all bosses with their IDs. -#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, FromPrimitive)] -pub enum Boss { - // Wing 1 - ValeGuardian = 0x3C4E, - Gorseval = 0x3C45, - Sabetha = 0x3C0F, - - // Wing 2 - Slothasor = 0x3EFB, - Matthias = 0x3EF3, - - // Wing 3 - KeepConstruct = 0x3F6B, - /// Xera ID for phase 1. - /// - /// This is only half of Xera's ID, as there will be a second agent for the - /// second phase. This agent will have another ID, see - /// [`XERA_PHASE2_ID`](constant.XERA_PHASE2_ID.html). - Xera = 0x3F76, - - // Wing 4 - Cairn = 0x432A, - MursaatOverseer = 0x4314, - Samarog = 0x4324, - Deimos = 0x4302, - - // Wing 5 - SoullessHorror = 0x4D37, - Dhuum = 0x4BFA, - - // Wing 6 - ConjuredAmalgamate = 0xABC6, - LargosTwins = 0x5271, - Qadim = 0x51C6, - - // Wing 7 - CardinalAdina = 0x55F6, - CardinalSabir = 0x55CC, - QadimThePeerless = 0x55F0, - - // 100 CM - Skorvald = 0x44E0, - Artsariiv = 0x461D, - Arkk = 0x455F, - - // 99 CM - MAMA = 0x427D, - Siax = 0x4284, - Ensolyss = 0x4234, - - // Strike missions - IcebroodConstruct = 0x568A, - VoiceOfTheFallen = 0x5747, - FraenirOfJormag = 0x57DC, - Boneskinner = 0x57F9, - WhisperOfJormag = 0x58B7, -} - - -/// Error for when converting a string to the boss fails. -#[derive(Debug, Clone, Hash, PartialEq, Eq)] -pub struct ParseBossError(String); - - -impl fmt::Display for ParseBossError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "Invalid boss identifier: {}", self.0) - } -} - - -impl FromStr for Boss { - type Err = ParseBossError; - - fn from_str(s: &str) -> Result<Self, Self::Err> { - let lower = s.to_lowercase(); - match &lower as &str { - "vg" | "vale guardian" => Ok(Boss::ValeGuardian), - "gorse" | "gorseval" => Ok(Boss::Gorseval), - "sab" | "sabetha" => Ok(Boss::Sabetha), - - "sloth" | "slothasor" => Ok(Boss::Slothasor), - "matthias" => Ok(Boss::Matthias), - - "kc" | "keep construct" => Ok(Boss::KeepConstruct), - "xera" => Ok(Boss::Xera), - - "cairn" => Ok(Boss::Cairn), - "mo" | "mursaat overseer" => Ok(Boss::MursaatOverseer), - "sam" | "sama" | "samarog" => Ok(Boss::Samarog), - "deimos" => Ok(Boss::Deimos), - - "desmina" | "sh" => Ok(Boss::SoullessHorror), - "dhuum" => Ok(Boss::Dhuum), - - "ca" | "conjured almagamate" => Ok(Boss::ConjuredAmalgamate), - "largos" | "twins" => Ok(Boss::LargosTwins), - "qadim" => Ok(Boss::Qadim), - - "adina" | "cardinal adina" => Ok(Boss::CardinalAdina), - "sabir" | "cardinal sabir" => Ok(Boss::CardinalSabir), - "qadimp" | "peerless qadim" | "qadim the peerless" => Ok(Boss::QadimThePeerless), - - "skorvald" => Ok(Boss::Skorvald), - "artsariiv" => Ok(Boss::Artsariiv), - "arkk" => Ok(Boss::Arkk), - - "mama" => Ok(Boss::MAMA), - "siax" => Ok(Boss::Siax), - "ensolyss" => Ok(Boss::Ensolyss), - - "icebrood" | "icebrood construct" => Ok(Boss::IcebroodConstruct), - "super kodan brothers" => Ok(Boss::VoiceOfTheFallen), - "fraenir" | "fraenir of jormag" => Ok(Boss::FraenirOfJormag), - "boneskinner" => Ok(Boss::Boneskinner), - "whisper" | "whisper of jormag" => Ok(Boss::WhisperOfJormag), - - _ => Err(ParseBossError(s.to_owned())) - } - } -} - - -/// ID for Xera in the second phase. -/// -/// The original Xera will despawn, and after the tower phase, a separate spawn -/// will take over. This new Xera will have this ID. Care needs to be taken when -/// calculating boss damage on this encounter, as both Xeras have to be taken -/// into account. -pub const XERA_PHASE2_ID: u16 = 0x3F9E; - -/// Contains a boon. -/// -/// Fields: -/// * boon id -/// * name (english) (just for easier debugging) -/// * maximum number of stacks -/// * boon type (intensity or duration) -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct Boon(pub u32, pub &'static str, pub u32, pub BoonType); - -impl Boon { - pub fn create_queue(&self) -> BoonQueue { - BoonQueue::new(self.2, self.3) - } -} - -/// A list of all boons (and conditions) -pub static BOONS: &[Boon] = &[ - // Standard boons. - // Boon queue sizes taken from the wiki: - // https://wiki.guildwars2.com/wiki/Effect_stacking - // IDs from wiki and skilldef.log: - // https://www.deltaconnected.com/arcdps/evtc/ - - // Duration based - Boon(743, "Aegis", 5, BoonType::Duration), - Boon(30328, "Alacrity", 9, BoonType::Duration), - Boon(725, "Fury", 9, BoonType::Duration), - Boon(717, "Protection", 5, BoonType::Duration), - Boon(718, "Regeneration", 5, BoonType::Duration), - Boon(26980, "Resistance", 5, BoonType::Duration), - Boon(873, "Retaliation", 5, BoonType::Duration), - Boon(719, "Swiftness", 9, BoonType::Duration), - Boon(1187, "Quickness", 5, BoonType::Duration), - Boon(726, "Vigor", 5, BoonType::Duration), - // Intensity based - Boon(740, "Might", 25, BoonType::Intensity), - Boon(1122, "Stability", 25, BoonType::Intensity), - // Standard conditions. - // Duration based - Boon(720, "Blinded", 5, BoonType::Duration), - Boon(722, "Chilled", 5, BoonType::Duration), - Boon(721, "Crippled", 5, BoonType::Duration), - Boon(791, "Fear", 5, BoonType::Duration), - Boon(727, "Immobile", 3, BoonType::Duration), - Boon(26766, "Slow", 3, BoonType::Duration), - Boon(742, "Weakness", 3, BoonType::Duration), - // Intensity based - Boon(736, "Bleeding", 1500, BoonType::Intensity), - Boon(737, "Burning", 1500, BoonType::Intensity), - Boon(861, "Confusion", 1500, BoonType::Intensity), - Boon(723, "Poison", 1500, BoonType::Intensity), - Boon(19426, "Torment", 1500, BoonType::Intensity), - Boon(738, "Vulnerability", 25, BoonType::Intensity), -]; - -pub fn get_boon(boon_id: u32) -> Option<&'static Boon> { - BOONS.iter().find(|b| b.0 == boon_id) -} - -/// Contains pre-defined triggers for boss mechanics. -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub enum Trigger { - /// Triggers when the given boon is applied to the player. - BoonPlayer(u32), - /// Triggers when the given boon is applied to the boss. - BoonBoss(u32), - /// Triggers when the given skill is used by a player. - SkillByPlayer(u32), - /// Triggers when the given skill is used on a player. - SkillOnPlayer(u32), - /// Triggers when the given boon is stripped from an enemy. - BoonStripped(u32), - /// Triggers when the given entity spawned. - Spawn(u16), - /// Triggers when the boss finishes channeling the given skill. - ChannelComplete(u32), -} - -/// Struct describing a boss mechanic. -/// -/// Fields: -/// * Boss id that this mechanic belongs to. -/// * How the mechanic is triggered. -/// * Technical term for the mechanic (for debugging purposes). -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct Mechanic(pub u16, pub Trigger, pub &'static str); - -impl Mechanic { - #[inline] - pub fn boss_id(&self) -> u16 { - self.0 - } - - #[inline] - pub fn trigger(&self) -> &Trigger { - &self.1 - } - - #[inline] - pub fn name(&self) -> &'static str { - self.2 - } -} - -macro_rules! mechanics { - ( $( $boss_id:expr => [ $($name:expr => $trigger:expr,)* ], )* ) => { - &[ - $( $(Mechanic($boss_id as u16, $trigger, $name)),* ),* - ] - } -} - -/// A slice of all mechanics that we know about. -pub static MECHANICS: &[Mechanic] = mechanics! { - // Wing 1 - Boss::ValeGuardian => [ - // Teleport: - "Unstable Magic Spike" => Trigger::SkillOnPlayer(31392), - ], - Boss::Gorseval => [ - // Slam - "Spectral Impact" => Trigger::SkillOnPlayer(31875), - // Egg - "Ghastly Prison" => Trigger::BoonPlayer(31623), - ], - Boss::Sabetha => [ - // Took the launch pad - "Shell-Shocked" => Trigger::BoonPlayer(34108), - ], - - // Wing 4 - Boss::Samarog => [ - "Prisoner Sweep" => Trigger::SkillOnPlayer(38168), - "Shockwave" => Trigger::SkillOnPlayer(37996), - ], -}; - -/// Get all mechanics that belong to the given boss. -pub fn get_mechanics(boss_id: u16) -> Vec<&'static Mechanic> { - MECHANICS.iter().filter(|m| m.0 == boss_id).collect() -} - - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - pub fn test_parsing() { - assert_eq!("vg".parse(), Ok(Boss::ValeGuardian)); - assert_eq!("VG".parse(), Ok(Boss::ValeGuardian)); - - assert!("vga".parse::<Boss>().is_err()); - } -} diff --git a/src/statistics/math.rs b/src/statistics/math.rs deleted file mode 100644 index a0849a3..0000000 --- a/src/statistics/math.rs +++ /dev/null @@ -1,240 +0,0 @@ -//! This module provides some basic mathematical structures. - -use std::iter; -use std::ops::{Mul, Sub}; - -/// A semigroup. -/// -/// This trait lets you combine elements by a binary operation. -pub trait Semigroup { - fn combine(&self, other: &Self) -> Self; -} - -/// A monoid. -/// -/// Extends the semigroup with a "neutral" element. -/// -/// # Laws -/// -/// ```raw -/// mempty.combine(x) == x -/// x.combine(mempty) == x -/// ``` -pub trait Monoid: Semigroup { - fn mempty() -> Self; -} - -#[derive(Debug, Clone)] -#[doc(hidden)] -pub struct Record<X, T, D> { - x: X, - tag: T, - data: D, -} - -/// A function that records tagged data points. -/// -/// This represents a "function" as a list of increases at soem discrete points. -/// Think about it as a generalized damage log. Increases can be tagged by some -/// arbitrary data, for example which the agent ID, the skill ID, the target, -/// ... -/// -/// This offers methods to get the value at a specific point (by "summing up" -/// all increments before that point), between two points and in total. It also -/// offers variants that allow you to filter the increments by their tag. -/// -/// Type parameters: -/// -/// * `X` domain of the function. Must have a defined `Ord`ering. -/// * `T` tag for each data point. Can be arbitrary. -/// * `D` actual data. Must be [`Monoid`](trait.Monoid.html), so that it can be -/// summed up. -#[derive(Clone)] -pub struct RecordFunc<X, T, D> { - data: Vec<Record<X, T, D>>, -} - -impl<X, T, D> RecordFunc<X, T, D> -where - X: Ord, - D: Monoid, -{ - /// Create a new `RecordFunc`. - pub fn new() -> Self { - RecordFunc { data: Vec::new() } - } - - #[doc(hidden)] - pub fn data(&self) -> &[Record<X, T, D>] { - &self.data - } - - /// Insert a data point into the record func. - /// - /// Note that you should supply the *increment*, not the *absolute value*! - pub fn insert(&mut self, x: X, tag: T, data: D) { - // Usually, the list will be built up in order, which means we can - // always append to the end. Check for this special case to make it - // faster. - if self.data.last().map(|r| r.x < x).unwrap_or(true) { - self.data.push(Record { x, tag, data }); - } else { - let index = match self.data.binary_search_by(|r| r.x.cmp(&x)) { - Ok(i) => i, - Err(i) => i, - }; - self.data.insert(index, Record { x, tag, data }); - } - //self.data.sort_by(|a, b| a.x.cmp(&b.x)); - } - - /// Get the amount of data points saved. - pub fn len(&self) -> usize { - self.data.len() - } - - /// Check whether there are no records. - pub fn is_emtpy(&self) -> bool { - self.data.is_empty() - } - - /// Get the absolute value at the specific point. - #[inline] - pub fn get(&self, x: &X) -> D { - self.get_only(x, |_| true) - } - - /// Get the absolute value at the specific point by only considering - /// increments where the predicate holds. - pub fn get_only<F: FnMut(&T) -> bool>(&self, x: &X, mut predicate: F) -> D { - self.data - .iter() - .take_while(|record| record.x <= *x) - .filter(|record| predicate(&record.tag)) - .fold(D::mempty(), |a, b| a.combine(&b.data)) - } - - /// Get the increments between the two given points. - #[inline] - pub fn between(&self, a: &X, b: &X) -> D { - self.between_only(a, b, |_| true) - } - - /// Get the increments between the two given points by only considering - /// increments where the predicate holds. - pub fn between_only<F: FnMut(&T) -> bool>(&self, a: &X, b: &X, mut predicate: F) -> D { - self.data - .iter() - .skip_while(|record| record.x < *a) - .take_while(|record| record.x <= *b) - .filter(|record| predicate(&record.tag)) - .fold(D::mempty(), |a, b| a.combine(&b.data)) - } - - /// Get the sum of all increments. - #[inline] - pub fn tally(&self) -> D { - self.tally_only(|_| true) - } - - /// Get the sum of all increments by only considering increments where the - /// predicate holds. - pub fn tally_only<F: FnMut(&T) -> bool>(&self, mut predicate: F) -> D { - self.data - .iter() - .filter(|record| predicate(&record.tag)) - .fold(D::mempty(), |a, b| a.combine(&b.data)) - } -} - -impl<X, T, D, A> RecordFunc<X, T, D> -where - X: Ord + Sub<X, Output = X> + Copy, - D: Monoid + Mul<X, Output = A>, - A: Monoid, -{ - #[inline] - pub fn integral(&self, a: &X, b: &X) -> A { - self.integral_only(a, b, |_| true) - } - - pub fn integral_only<F: FnMut(&T) -> bool>(&self, a: &X, b: &X, mut predicate: F) -> A { - let points = self - .data - .iter() - .skip_while(|record| record.x < *a) - .take_while(|record| record.x <= *b) - .filter(|record| predicate(&record.tag)) - .map(|record| record.x) - .chain(iter::once(*b)) - .collect::<Vec<_>>(); - let mut area = A::mempty(); - let mut last = *a; - for point in points { - let diff = point - last; - let value = self.get_only(&last, &mut predicate); - area = area.combine(&(value * diff)); - last = point; - } - area - } -} - -impl<X: Ord, T, D: Monoid> Default for RecordFunc<X, T, D> { - fn default() -> Self { - Self::new() - } -} - -#[cfg(test)] -mod test { - use super::*; - - #[derive(Debug, PartialEq, Eq)] - struct Integer(u32); - - impl Semigroup for Integer { - fn combine(&self, other: &Self) -> Self { - Integer(self.0 + other.0) - } - } - - impl Monoid for Integer { - fn mempty() -> Self { - Integer(0) - } - } - - fn create() -> RecordFunc<u32, u8, Integer> { - let mut result = RecordFunc::new(); - - result.insert(6, 1, Integer(6)); - result.insert(4, 0, Integer(5)); - result.insert(0, 1, Integer(3)); - result.insert(2, 0, Integer(4)); - - result - } - - #[test] - fn recordfunc_get() { - let rf = create(); - - assert_eq!(rf.get(&3), Integer(7)); - assert_eq!(rf.get(&4), Integer(12)); - } - - #[test] - fn recordfunc_get_only() { - let rf = create(); - - assert_eq!(rf.get_only(&3, |t| *t == 0), Integer(4)); - } - - #[test] - fn recordfunc_between() { - let rf = create(); - - assert_eq!(rf.between(&1, &5), Integer(9)); - } -} diff --git a/src/statistics/mechanics.rs b/src/statistics/mechanics.rs deleted file mode 100644 index 0cf6f24..0000000 --- a/src/statistics/mechanics.rs +++ /dev/null @@ -1,62 +0,0 @@ -use super::gamedata::Mechanic; -use super::math::{Monoid, RecordFunc, Semigroup}; - -use std::fmt; - -/// A simple wrapper for integers. -#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] -struct Counter(u32); - -impl Semigroup for Counter { - #[inline] - fn combine(&self, other: &Counter) -> Counter { - Counter(self.0 + other.0) - } -} - -impl Monoid for Counter { - #[inline] - fn mempty() -> Counter { - Counter(0) - } -} - -/// Provides access to the mechanic log. -#[derive(Clone, Default)] -pub struct MechanicLog { - inner: RecordFunc<u64, (&'static Mechanic, u64), Counter>, -} - -impl MechanicLog { - /// Increase the mechanic counter for the given mechanic and agent by one. - pub fn increase(&mut self, time: u64, mechanic: &'static Mechanic, agent: u64) { - self.inner.insert(time, (mechanic, agent), Counter(1)); - } - - /// Return the count of mechanics. - /// - /// A function can be provided to filter entries by mechanic type and agent. - pub fn count<F: FnMut(&'static Mechanic, u64) -> bool>(&self, mut filter: F) -> u32 { - self.inner.tally_only(|(a, b)| filter(a, *b)).0 - } - - /// Return the count of mechanics between the two given times. - /// - /// A function can be provided to filter entries by mechanic type and agent. - pub fn count_between<F: FnMut(&'static Mechanic, u64) -> bool>( - &self, - start: u64, - stop: u64, - mut filter: F, - ) -> u32 { - self.inner - .between_only(&start, &stop, |(a, b)| filter(a, *b)) - .0 - } -} - -impl fmt::Debug for MechanicLog { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "MechanicLog {{ ... }}") - } -} diff --git a/src/statistics/mod.rs b/src/statistics/mod.rs deleted file mode 100644 index 3e42d9c..0000000 --- a/src/statistics/mod.rs +++ /dev/null @@ -1,145 +0,0 @@ -//! This module aids in the creation of actual boss statistics. -use super::*; -use std::collections::HashMap; -use thiserror::Error; - -pub mod boon; -pub mod damage; -pub mod gamedata; -pub mod math; -pub mod mechanics; -pub mod trackers; - -use self::boon::BoonLog; -use self::damage::DamageLog; -use self::mechanics::MechanicLog; -use self::trackers::{RunnableTracker, Tracker}; - -pub type StatResult<T> = Result<T, StatError>; - -#[derive(Error, Debug)] -pub enum StatError { - #[error("tracker returned an error: {0}")] - TrackerError(#[from] Box<dyn std::error::Error>), -} - -macro_rules! try_tracker { - ($expr:expr) => { - #[allow(unreachable_code)] - match $expr { - Ok(e) => e, - Err(e) => return Err(StatError::TrackerError(e)), - } - }; -} - -/// A struct containing the calculated statistics for the log. -#[derive(Clone, Debug)] -pub struct Statistics { - /// The complete damage log. - pub damage_log: DamageLog, - /// The complete mechanics log. - pub mechanic_log: MechanicLog, - /// A map mapping agent addresses to their stats. - pub agent_stats: HashMap<u64, AgentStats>, -} - -/// A struct describing the agent statistics. -#[derive(Clone, Debug, Default)] -pub struct AgentStats { - /// Average stacks of boons. - /// - /// This also includes conditions. - /// - /// For duration-based boons, the average amount of stacks is the same as - /// the uptime. - pub boon_log: BoonLog, - /// Time when the agent has entered combat (millseconds since log start). - pub enter_combat: u64, - /// Time when the agent has left combat (millseconds since log start). - pub exit_combat: u64, -} - -impl AgentStats { - /// Returns the combat time of this agent in milliseconds. - pub fn combat_time(&self) -> u64 { - self.exit_combat - self.enter_combat - } -} - -/// Takes a bunch of trackers and runs them on the given log. -/// -/// This method returns "nothing", as the statistics are saved in the trackers. -/// It's the job of the caller to extract them appropriately. -pub fn run_trackers(log: &Log, trackers: &mut [&mut dyn RunnableTracker]) -> StatResult<()> { - for event in log.events() { - for tracker in trackers.iter_mut() { - try_tracker!((*tracker).run_feed(event)); - } - } - Ok(()) -} - -/// Calculate the statistics for the given log. -pub fn calculate(log: &Log) -> StatResult<Statistics> { - let mut agent_stats = HashMap::<u64, AgentStats>::new(); - - let mut damage_tracker = trackers::DamageTracker::new(log); - let mut log_start_tracker = trackers::LogStartTracker::new(); - let mut combat_time_tracker = trackers::CombatTimeTracker::new(); - let mut boon_tracker = trackers::BoonTracker::new(); - - let mechanics = gamedata::get_mechanics(log.boss_id); - let boss_addr = log.boss_agents().into_iter().map(|x| *x.addr()).collect(); - let mut mechanic_tracker = trackers::MechanicTracker::new(boss_addr, mechanics); - - run_trackers( - log, - &mut [ - &mut damage_tracker, - &mut log_start_tracker, - &mut combat_time_tracker, - &mut boon_tracker, - &mut mechanic_tracker, - ], - )?; - - let log_start_time = try_tracker!(log_start_tracker.finalize()); - - let combat_times = try_tracker!(combat_time_tracker.finalize()); - for (agent_addr, &(enter_time, exit_time)) in &combat_times { - let agent = agent_stats - .entry(*agent_addr) - .or_insert_with(Default::default); - // XXX: This used to be enter_time - log_start_time, as it makes more - // sense to have the time relative to the log start instead of the - // Windows boot time. However, this also means that we need to modify - // all event times before we do any tracking, as many trackers rely on - // event.time to track information related to time. - if enter_time != 0 { - agent.enter_combat = enter_time; - } else { - agent.enter_combat = log_start_time; - } - if exit_time != 0 { - agent.exit_combat = exit_time; - } - } - - let boon_logs = try_tracker!(boon_tracker.finalize()); - for (agent_addr, boon_log) in boon_logs { - let agent = agent_stats - .entry(agent_addr) - .or_insert_with(Default::default); - agent.boon_log = boon_log; - } - - let damage_log = try_tracker!(damage_tracker.finalize()); - let mechanic_log = try_tracker!(mechanic_tracker.finalize()); - - Ok(Statistics { - damage_log, - mechanic_log, - agent_stats, - }) -} diff --git a/src/statistics/trackers.rs b/src/statistics/trackers.rs deleted file mode 100644 index 16cb755..0000000 --- a/src/statistics/trackers.rs +++ /dev/null @@ -1,443 +0,0 @@ -//! evtclib tracker definitions. -//! -//! The idea behind a "tracker" is to have one object taking care of one -//! specific thing. This makes it easier to organize the whole "statistic -//! gathering loop", and it keeps each tracker somewhat small. -//! -//! It's also easy to define your own trackers if there are any statistics that -//! you want to track. Just implement [`Tracker`](trait.Tracker.html). It -//! doesn't matter what you track, it doesn't matter how many trackers you -//! define. -//! -//! If you want to track stats separated by player or phases, consider writing -//! your tracker in a way that it only tracks statistics for a single player, -//! and then use a [`Multiplexer`](struct.Multiplexer.html) to automatically -//! track it for every player/agent. -//! -//! You can use [`run_trackers`](../fn.run_trackers.html) to run multiple -//! trackers on the same log. -use std::collections::HashMap; -use std::error::Error; - -use super::super::{Event, EventKind, Log}; -use super::boon::{BoonLog, BoonQueue}; -use super::damage::{DamageLog, DamageType}; -use super::gamedata::{self, Mechanic, Trigger}; -use super::mechanics::MechanicLog; - -use super::super::raw::CbtResult; - -use fnv::FnvHashMap; - -/// A tracker. -/// -/// A tracker should be responsible for tracking a single statistic. Each -/// tracker is fed each event. If an error is returned while feeding, the whole -/// calculation will be aborted. -pub trait Tracker { - /// The resulting statistic that this tracker will return. - type Stat; - /// The error that this tracker might return. - type Error: Error; - - /// Feed a single event into this tracker. - /// - /// The tracker will update its internal state. - fn feed(&mut self, event: &Event) -> Result<(), Self::Error>; - - /// Finalize this tracker and get the statistics out. - fn finalize(self) -> Result<Self::Stat, Self::Error>; -} - -/// A helper trait that erases the types from a tracker. -/// -/// This makes it able to use references like `&mut RunnableTracker` without -/// having to specify the generic types, allowing you to e.g. store a bunch of -/// them in a vector, regardless of their output type. Unless you want to do -/// that, you probably don't want to use this trait directly. -/// -/// Note that you do not need to implement this yourself. It is automatically -/// implemented for all types that implement `Tracker`. -/// -/// RunnableTrackers provide no way to extract their resources, and they wrap -/// all errors in `Box<_>`, so you should always keep a "real" reference around -/// if you plan on getting any data. -pub trait RunnableTracker { - /// See `Tracker.feed()`. Renamed to avoid conflicts. - fn run_feed(&mut self, event: &Event) -> Result<(), Box<dyn Error>>; -} - -impl<S, E: Error + 'static, T: Tracker<Stat = S, Error = E>> RunnableTracker for T { - fn run_feed(&mut self, event: &Event) -> Result<(), Box<dyn Error>> { - self.feed(event).map_err(|e| Box::new(e) as Box<dyn Error>) - } -} - -/// A tracker that tracks per-target damage of all agents. -pub struct DamageTracker<'l> { - log: &'l Log, - damage_log: DamageLog, -} - -impl<'t> DamageTracker<'t> { - /// Create a new damage tracker for the given log. - pub fn new(log: &Log) -> DamageTracker { - DamageTracker { - log, - damage_log: DamageLog::new(), - } - } -} - -impl<'t> Tracker for DamageTracker<'t> { - type Stat = DamageLog; - type Error = !; - - fn feed(&mut self, event: &Event) -> Result<(), Self::Error> { - match event.kind { - EventKind::Physical { - source_agent_addr, - destination_agent_addr, - damage, - skill_id, - .. - } => { - let source = if let Some(master) = self.log.master_agent(source_agent_addr) { - master.addr - } else { - source_agent_addr - }; - self.damage_log.log( - event.time, - source, - destination_agent_addr, - DamageType::Physical, - skill_id, - damage as u64, - ); - } - - EventKind::ConditionTick { - source_agent_addr, - destination_agent_addr, - damage, - condition_id, - .. - } => { - let source = if let Some(master) = self.log.master_agent(source_agent_addr) { - master.addr - } else { - source_agent_addr - }; - self.damage_log.log( - event.time, - source, - destination_agent_addr, - DamageType::Condition, - condition_id, - damage as u64, - ); - } - - _ => (), - } - Ok(()) - } - - fn finalize(self) -> Result<Self::Stat, Self::Error> { - Ok(self.damage_log) - } -} - -/// Tracks when the log has been started. -#[derive(Default)] -pub struct LogStartTracker { - event_time: u64, -} - -impl LogStartTracker { - /// Create a new log start tracker. - pub fn new() -> LogStartTracker { - LogStartTracker { event_time: 0 } - } -} - -impl Tracker for LogStartTracker { - type Stat = u64; - type Error = !; - - fn feed(&mut self, event: &Event) -> Result<(), Self::Error> { - if let EventKind::LogStart { .. } = event.kind { - self.event_time = event.time; - } - Ok(()) - } - - fn finalize(self) -> Result<Self::Stat, Self::Error> { - Ok(self.event_time) - } -} - -/// A tracker that tracks the combat entry and exit times for each agent. -#[derive(Default)] -pub struct CombatTimeTracker { - times: HashMap<u64, (u64, u64)>, -} - -impl CombatTimeTracker { - /// Create a new combat time tracker. - pub fn new() -> CombatTimeTracker { - Default::default() - } -} - -impl Tracker for CombatTimeTracker { - // Maps from agent addr to (entry time, exit time) - type Stat = HashMap<u64, (u64, u64)>; - type Error = !; - - fn feed(&mut self, event: &Event) -> Result<(), Self::Error> { - match event.kind { - EventKind::EnterCombat { agent_addr, .. } => { - self.times.entry(agent_addr).or_insert((0, 0)).0 = event.time; - } - - EventKind::ExitCombat { agent_addr } => { - self.times.entry(agent_addr).or_insert((0, 0)).1 = event.time; - } - - _ => (), - } - Ok(()) - } - - fn finalize(self) -> Result<Self::Stat, Self::Error> { - Ok(self.times) - } -} - -/// A tracker that tracks the total "boon area" per agent. -/// -/// The boon area is defined as the amount of stacks multiplied by the time. So -/// 1 stack of Might for 1000 milliseconds equals 1000 "stackmilliseconds" of -/// Might. You can use this boon area to calculate the average amount of stacks -/// by taking the boon area and dividing it by the combat time. -/// -/// Note that this also tracks conditions, because internally, they're handled -/// the same way. -/// -/// This tracker only tracks the boons that are known to evtclib, that is the -/// boons defined in `evtclib::statistics::gamedata::BOONS`. -pub struct BoonTracker { - boon_logs: FnvHashMap<u64, BoonLog>, - boon_queues: FnvHashMap<u64, FnvHashMap<u32, BoonQueue>>, - last_time: u64, -} - -impl BoonTracker { - /// Creates a new boon tracker for the given agent. - pub fn new() -> BoonTracker { - BoonTracker { - boon_logs: Default::default(), - boon_queues: Default::default(), - last_time: 0, - } - } - - /// Updates the internal boon queues by the given amount of milliseconds. - /// - /// * `delta_t` - Amount of milliseconds to update. - fn update_queues(&mut self, delta_t: u64) { - if delta_t == 0 { - return; - } - - self.boon_queues - .values_mut() - .flat_map(|m| m.values_mut()) - .for_each(|queue| queue.simulate(delta_t)); - } - - fn cleanup_queues(&mut self) { - // Throw away empty boon queues or to improve performance - self.boon_queues - .values_mut() - .for_each(|m| m.retain(|_, q| !q.is_empty())); - self.boon_queues.retain(|_, q| !q.is_empty()); - } - - fn update_logs(&mut self, time: u64) { - for (agent, boons) in &self.boon_queues { - let agent_log = self - .boon_logs - .entry(*agent) - .or_insert_with(Default::default); - for (boon_id, queue) in boons { - agent_log.log(time, *boon_id, queue.current_stacks()); - } - } - } - - /// Get the boon queue for the given buff_id. - /// - /// If the queue does not yet exist, create it. - /// - /// * `agent_addr` - The address of the agent. - /// * `buff_id` - The buff (or condition) id. - fn get_queue(&mut self, agent_addr: u64, buff_id: u32) -> Option<&mut BoonQueue> { - use std::collections::hash_map::Entry; - let entry = self - .boon_queues - .entry(agent_addr) - .or_insert_with(Default::default) - .entry(buff_id); - match entry { - // Queue already exists - Entry::Occupied(e) => Some(e.into_mut()), - // Queue needs to be created, but only if we know about that boon. - Entry::Vacant(e) => { - let boon_queue = gamedata::get_boon(buff_id).map(gamedata::Boon::create_queue); - if let Some(queue) = boon_queue { - Some(e.insert(queue)) - } else { - None - } - } - } - } -} - -impl Tracker for BoonTracker { - type Stat = HashMap<u64, BoonLog>; - type Error = !; - - fn feed(&mut self, event: &Event) -> Result<(), Self::Error> { - let delta_t = event.time - self.last_time; - self.update_queues(delta_t); - - match event.kind { - EventKind::BuffApplication { - destination_agent_addr, - buff_id, - duration, - .. - } => { - if let Some(queue) = self.get_queue(destination_agent_addr, buff_id) { - queue.add_stack(duration as u64); - } - } - - // XXX: Properly handle SINGLE and MANUAL removal types - EventKind::BuffRemove { - destination_agent_addr, - buff_id, - .. - } => { - if let Some(queue) = self.get_queue(destination_agent_addr, buff_id) { - queue.clear(); - } - } - - _ => (), - } - - self.update_logs(event.time); - self.last_time = event.time; - self.cleanup_queues(); - - Ok(()) - } - - fn finalize(self) -> Result<Self::Stat, Self::Error> { - // Convert from FnvHashMap to HashMap in order to not leak - // implementation details. - Ok(self.boon_logs.into_iter().collect()) - } -} - -/// A tracker that tracks boss mechanics for each player. -pub struct MechanicTracker { - log: MechanicLog, - available_mechanics: Vec<&'static Mechanic>, - boss_addresses: Vec<u64>, -} - -impl MechanicTracker { - /// Create a new mechanic tracker that watches over the given mechanics. - pub fn new(boss_addresses: Vec<u64>, mechanics: Vec<&'static Mechanic>) -> MechanicTracker { - MechanicTracker { - log: MechanicLog::default(), - available_mechanics: mechanics, - boss_addresses, - } - } -} - -impl MechanicTracker { - fn is_boss(&self, addr: u64) -> bool { - self.boss_addresses.contains(&addr) - } -} - -impl Tracker for MechanicTracker { - type Stat = MechanicLog; - type Error = !; - - fn feed(&mut self, event: &Event) -> Result<(), Self::Error> { - for mechanic in &self.available_mechanics { - match (&event.kind, &mechanic.1) { - ( - EventKind::Physical { - source_agent_addr, - destination_agent_addr, - skill_id, - result, - .. - }, - Trigger::SkillOnPlayer(trigger_id), - ) - if skill_id == trigger_id - && self.is_boss(*source_agent_addr) - && *result != CbtResult::Evade - && *result != CbtResult::Absorb - && *result != CbtResult::Block => - { - self.log - .increase(event.time, mechanic, *destination_agent_addr); - } - - ( - EventKind::BuffApplication { - destination_agent_addr, - buff_id, - .. - }, - Trigger::BoonPlayer(trigger_id), - ) - if buff_id == trigger_id => - { - // Some buff applications are registered multiple times. So - // instead of counting those quick successions separately - // (and thus having a wrong count), we check if this - // mechanic has already been logged "shortly before" (10 millisecons). - if self - .log - .count_between(event.time - 10, event.time + 1, |m, w| { - &m == mechanic && w == *destination_agent_addr - }) - == 0 - { - self.log - .increase(event.time, mechanic, *destination_agent_addr); - } - } - _ => (), - } - } - Ok(()) - } - - fn finalize(self) -> Result<Self::Stat, Self::Error> { - Ok(self.log) - } -} |