diff --git a/Cargo.lock b/Cargo.lock index 5f2d73e..0466d5f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -70,7 +70,7 @@ dependencies = [ [[package]] name = "arcdps_buddy" -version = "0.5.2" +version = "0.6.0" dependencies = [ "arc_util", "arcdps", diff --git a/Cargo.toml b/Cargo.toml index 60a70b3..adf88cd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "arcdps_buddy" -version = "0.5.2" +version = "0.6.0" edition = "2021" authors = ["Zerthox"] repository = "https://github.com/zerthox/arcdps-buddy" diff --git a/src/combat/agent.rs b/src/combat/agent.rs index cf97285..a88c056 100644 --- a/src/combat/agent.rs +++ b/src/combat/agent.rs @@ -44,3 +44,9 @@ impl From<&Agent<'_>> for Target { Self::new(kind, name) } } + +impl PartialEq for Target { + fn eq(&self, other: &Self) -> bool { + self.kind == other.kind + } +} diff --git a/src/combat/transfer.rs b/src/combat/transfer.rs index 54fc299..19d01ac 100644 --- a/src/combat/transfer.rs +++ b/src/combat/transfer.rs @@ -3,23 +3,27 @@ use log::debug; pub use crate::data::Condition; +/// Error margin for times. +pub const TIME_EPSILON: u32 = 10; + /// Transfer tracking. #[derive(Debug, Clone)] pub struct TransferTracker { /// Detected transfers. - pub transfers: Vec, + transfers: Vec, /// Condition removes. remove: Vec, /// Condition applies as transfer candidates. - apply: Vec, + apply: Vec, } impl TransferTracker { /// Time to retain candidates. pub const RETAIN_TIME: i32 = 100; + /// Creates a new transfer tracker. pub const fn new() -> Self { Self { transfers: Vec::new(), @@ -28,38 +32,60 @@ impl TransferTracker { } } - /// Adds a condition remove. + /// Returns an iterator over found condition transfers. + pub fn found(&self) -> &[Transfer] { + &self.transfers + } + + /// Adds a new condition remove. pub fn add_remove(&mut self, remove: Remove) { self.purge(remove.time); - if let Some(apply) = Self::find_remove(&mut self.apply, |apply| apply.matches(&remove)) { - debug!("transfer: {remove:?} matches {apply:?}"); - self.transfers.push(apply) + debug!("transfer candidate {remove:?}"); + if let Some(apply) = Self::find_take(&mut self.apply, |apply| apply.matches(&remove)) { + debug!("transfer match {remove:?} {apply:?}"); + self.add_transfer(apply) } else { self.remove.push(remove) } } - /// Adds a condition apply as transfer candidate. - pub fn add_apply(&mut self, apply: Transfer) { + /// Adds a new condition apply. + pub fn add_apply(&mut self, apply: Apply) { self.purge(apply.time); - if let Some(remove) = Self::find_remove(&mut self.remove, |remove| apply.matches(remove)) { - debug!("transfer: {apply:?} matches {remove:?}"); - self.transfers.push(apply) + debug!("transfer candidate {apply:?}"); + if let Some(remove) = Self::find_take(&mut self.remove, |remove| apply.matches(remove)) { + debug!("transfer match {apply:?} {remove:?}"); + self.add_transfer(apply) } else { self.apply.push(apply) } } - fn find_remove(vec: &mut Vec, pred: impl FnMut(&T) -> bool) -> Option { - if let Some(index) = vec.iter().position(pred) { - Some(vec.swap_remove(index)) + /// Adds a new transfer. + fn add_transfer(&mut self, apply: Apply) { + let transfer = Transfer::from(apply); + if let Some(existing) = self + .transfers + .iter_mut() + .find(|other| transfer.is_group(other)) + { + existing.stacks += 1; + debug!("transfer update {existing:?}"); } else { - None + debug!("transfer new {transfer:?}"); + self.transfers.push(transfer) } } + /// Find an element matching the predicate and take it out of the [`Vec`]. + fn find_take(vec: &mut Vec, pred: impl FnMut(&T) -> bool) -> Option { + vec.iter() + .position(pred) + .map(|index| vec.swap_remove(index)) + } + /// Purges old information. - fn purge(&mut self, now: i32) { + pub fn purge(&mut self, now: i32) { self.remove.retain(|el| Self::check_time(el.time, now)); self.apply.retain(|el| Self::check_time(el.time, now)); } @@ -76,6 +102,41 @@ impl Default for TransferTracker { } } +/// Information about a condition apply. +#[derive(Debug, Clone)] +pub struct Apply { + /// Time of the apply. + pub time: i32, + + /// Condition applied. + pub condi: Condition, + + /// Duration applied. + pub duration: i32, + + /// Target the condition was applied to. + pub target: Target, +} + +impl Apply { + /// Creates a new condition apply. + pub fn new(time: i32, condi: Condition, duration: i32, target: Target) -> Self { + Self { + time, + condi, + duration, + target, + } + } + + /// Check whether the apply matches a remove. + pub fn matches(&self, remove: &Remove) -> bool { + self.condi == remove.condi + && self.duration.abs_diff(remove.time) < TIME_EPSILON + && self.time.abs_diff(remove.time) < TIME_EPSILON + } +} + /// Information about a condition remove. #[derive(Debug, Clone)] pub struct Remove { @@ -85,17 +146,17 @@ pub struct Remove { /// Condition removed. pub condi: Condition, - /// Amount of stacks removed. - pub stacks: u32, + /// Duration removed. + pub duration: i32, } impl Remove { /// Creates a new condition transfer. - pub fn new(time: i32, condi: Condition, stacks: u32) -> Self { + pub fn new(time: i32, condi: Condition, duration: i32) -> Self { Self { time, condi, - stacks, + duration, } } } @@ -117,24 +178,21 @@ pub struct Transfer { } impl Transfer { - /// Error margin for transfer times. - pub const TIME_EPSILON: i32 = 10; + /// Check whether the transfers should be grouped. + pub fn is_group(&self, other: &Self) -> bool { + self.condi == other.condi + && self.target == other.target + && self.time.abs_diff(other.time) < TIME_EPSILON + } +} - /// Creates a new condition transfer. - pub fn new(time: i32, condi: Condition, stacks: u32, target: Target) -> Self { +impl From for Transfer { + fn from(apply: Apply) -> Self { Self { - time, - condi, - stacks, - - target, + time: apply.time, + condi: apply.condi, + stacks: 1, + target: apply.target, } } - - /// Check whether the transfer candidate matches a remove. - pub fn matches(&self, remove: &Remove) -> bool { - self.condi == remove.condi - && self.stacks == remove.stacks - && (self.time - remove.time) <= Self::TIME_EPSILON - } } diff --git a/src/history/fight.rs b/src/history/fight.rs index fa2437d..25231c6 100644 --- a/src/history/fight.rs +++ b/src/history/fight.rs @@ -82,4 +82,12 @@ impl Fight { self.end = Some(time); time - self.start } + + /// Calculates the timestamp as relative time to the fight start. + pub fn relative_time(&self, time: u64) -> Option { + match self.end { + Some(end) if time > end => None, + _ => Some((time - self.start) as i32), + } + } } diff --git a/src/history/mod.rs b/src/history/mod.rs index e5633d7..c74fd51 100644 --- a/src/history/mod.rs +++ b/src/history/mod.rs @@ -83,13 +83,21 @@ impl History { self.viewed = 0; } } -} -#[allow(unused)] -impl History -where - T: Default, -{ + /// Calculates relative time to start for the latest fight. + pub fn relative_time(&self, time: u64) -> Option { + // TODO: handle timestamp in previous fight + self.latest_fight() + .and_then(|fight| fight.relative_time(time)) + } + + /// Returns the latest fight and the relative time to fight start. + pub fn fight_and_time(&mut self, time: u64) -> Option<(i32, &mut Fight)> { + // TODO: handle timestamp in previous fight + self.latest_fight_mut() + .and_then(|fight| fight.relative_time(time).map(|time| (time, fight))) + } + /// Adds a fight to the history. pub fn add_fight(&mut self, fight: Fight) { if let Some(prev) = self.fights.front() { @@ -108,19 +116,28 @@ where } /// Adds a fight with default data to the history. - pub fn add_fight_default(&mut self, time: u64) { + pub fn add_fight_default(&mut self, time: u64) + where + T: Default, + { self.add_fight(Fight::new(time, T::default())) } /// Adds a fight with default data and target information to the history. - pub fn add_fight_with_target(&mut self, time: u64, species: u32, target: Option<&Agent>) { + pub fn add_fight_with_target(&mut self, time: u64, species: u32, target: Option<&Agent>) + where + T: Default, + { self.add_fight(Fight::with_target(time, species, target, T::default())); } /// Updates the target for the latest fight. /// /// If there is no fight present or the latest fight already ended, a new fight with the target is added instead. - pub fn update_fight_target(&mut self, time: u64, species: u32, target: Option<&Agent>) { + pub fn update_fight_target(&mut self, time: u64, species: u32, target: Option<&Agent>) + where + T: Default, + { match self.latest_fight_mut() { Some(fight @ Fight { end: None, .. }) => fight.update_target(species, target), _ => self.add_fight_with_target(time, species, target), diff --git a/src/plugin/event.rs b/src/plugin/event.rs index 019f355..062f386 100644 --- a/src/plugin/event.rs +++ b/src/plugin/event.rs @@ -4,9 +4,9 @@ use crate::combat::{ buff::{Buff, BuffApply}, cast::{Cast, CastState}, skill::Skill, - transfer::{Condition, Remove, Transfer}, + transfer::{Apply, Condition, Remove}, }; -use arcdps::{evtc::EventKind, Activation, Agent, CombatEvent, StateChange, Strike}; +use arcdps::{evtc::EventKind, Activation, Agent, BuffRemove, CombatEvent, StateChange, Strike}; use log::debug; impl Plugin { @@ -32,7 +32,7 @@ impl Plugin { EventKind::Activation if src_self => { let mut plugin = Self::lock(); - if let Some(time) = plugin.combat_time(&event) { + if let Some(time) = plugin.history.relative_time(event.time) { if plugin.data.contains(event.skill_id) { match event.is_activation { Activation::Start => { @@ -60,8 +60,8 @@ impl Plugin { Self::lock().apply_buff(&event, buff, &src, &dst) } } else if let Ok(condi) = buff.try_into() { - // only care about condis sourced from self - if src_self { + // only care about condis from self and ignore extensions + if src_self && event.is_off_cycle == 0 { Self::lock().apply_condi(&event, condi, &dst) } } @@ -70,14 +70,13 @@ impl Plugin { EventKind::BuffRemove => { if let Some(dst) = dst { - // only care about self removes - // TODO: verify src dst for transfers - if src_self && dst.is_self != 0 { + // only care about removes from self to self + if event.is_buff_remove == BuffRemove::Manual + && src_self + && dst.is_self != 0 + { if let Ok(condi) = event.skill_id.try_into() { - let mut plugin = Self::lock(); - if let Some(time) = plugin.combat_time(&event) { - plugin.remove_buff(condi, 1, time); - } + Self::lock().remove_buff(&event, condi) } } } @@ -87,7 +86,9 @@ impl Plugin { let mut plugin = Self::lock(); let is_minion = plugin.is_own_minion(&event); if src_self || is_minion { - if let (Some(dst), Some(time)) = (dst, plugin.combat_time(&event)) { + if let (Some(dst), Some(time)) = + (dst, plugin.history.relative_time(event.time)) + { plugin.strike(&event, is_minion, skill_name, &dst, time) } } @@ -114,17 +115,10 @@ impl Plugin { } } - fn combat_time(&self, event: &CombatEvent) -> Option { - // TODO: add data to previous fight? - self.start - .filter(|start| event.time >= *start) - .map(|start| (event.time - start) as i32) - } - fn start_fight(&mut self, event: CombatEvent, target: Option) { let species = event.src_agent as u32; debug!("log start for {species}, {target:?}"); - self.start = Some(event.time); + self.history .add_fight_with_target(event.time, species, target.as_ref()); } @@ -139,7 +133,6 @@ impl Plugin { fn end_fight(&mut self, event: CombatEvent, target: Option) { let species = event.src_agent; debug!("log end for {species}, {target:?}"); - self.start = None; self.history.end_latest_fight(event.time); } @@ -189,10 +182,8 @@ impl Plugin { } fn apply_buff(&mut self, event: &CombatEvent, buff: Buff, src: &Agent, dst: &Agent) { - if src.is_self != 0 || self.is_own_minion(&event) { - if let (Some(time), Some(fight)) = - (self.combat_time(&event), self.history.latest_fight_mut()) - { + if src.is_self != 0 || self.is_own_minion(event) { + if let Some((time, fight)) = self.history.fight_and_time(event.time) { // TODO: "effective" duration excluding overstack? let duration = event.value; let apply = BuffApply::new(time, buff, duration, dst.into()); @@ -202,17 +193,15 @@ impl Plugin { } fn apply_condi(&mut self, event: &CombatEvent, condi: Condition, target: &Agent) { - if let (Some(time), Some(fight)) = - (self.combat_time(&event), self.history.latest_fight_mut()) - { - let apply = Transfer::new(time, condi, 1, target.into()); + if let Some((time, fight)) = self.history.fight_and_time(event.time) { + let apply = Apply::new(time, condi, event.value, target.into()); fight.data.transfers.add_apply(apply); } } - fn remove_buff(&mut self, condi: Condition, stacks: u32, time: i32) { - if let Some(fight) = self.history.latest_fight_mut() { - let remove = Remove::new(time, condi, stacks); + fn remove_buff(&mut self, event: &CombatEvent, condi: Condition) { + if let Some((time, fight)) = self.history.fight_and_time(event.time) { + let remove = Remove::new(time, condi, event.value); fight.data.transfers.add_remove(remove) } } diff --git a/src/plugin/mod.rs b/src/plugin/mod.rs index 6a6fe61..160d5d7 100644 --- a/src/plugin/mod.rs +++ b/src/plugin/mod.rs @@ -5,7 +5,10 @@ use crate::{ combat::CombatData, data::{LoadError, SkillData}, history::History, - ui::{breakbar_log::BreakbarLog, buff_log::BuffLog, cast_log::CastLog, multi_view::MultiView}, + ui::{ + breakbar_log::BreakbarLog, buff_log::BuffLog, cast_log::CastLog, multi_view::MultiView, + transfer_log::TransferLog, + }, }; use arc_util::{ settings::Settings, @@ -38,7 +41,6 @@ pub struct Plugin { data: SkillData, data_state: Result, - start: Option, self_instance_id: Option, history: History, @@ -46,11 +48,18 @@ pub struct Plugin { cast_log: Window, buff_log: Window, breakbar_log: Window, + transfer_log: Window, } impl Plugin { /// Creates a new plugin. pub fn new() -> Self { + let options = WindowOptions { + width: 350.0, + height: 450.0, + ..Default::default() + }; + Self { updater: Updater::new( "Buddy", @@ -61,34 +70,12 @@ impl Plugin { data: SkillData::with_defaults(), data_state: Err(LoadError::NotFound), - start: None, self_instance_id: None, history: History::new(10, 5000, true), - multi_view: Window::with_default( - "Buddy Multi", - WindowOptions { - width: 350.0, - height: 450.0, - ..Default::default() - }, - ), - cast_log: Window::with_default( - "Buddy Casts", - WindowOptions { - width: 350.0, - height: 450.0, - ..Default::default() - }, - ), - buff_log: Window::with_default( - "Buddy Buffs", - WindowOptions { - width: 350.0, - height: 450.0, - ..Default::default() - }, - ), + multi_view: Window::with_default("Buddy Multi", options.clone()), + cast_log: Window::with_default("Buddy Casts", options.clone()), + buff_log: Window::with_default("Buddy Buffs", options.clone()), breakbar_log: Window::with_default( "Buddy Breakbar", WindowOptions { @@ -97,6 +84,7 @@ impl Plugin { ..Default::default() }, ), + transfer_log: Window::with_default("Buddy Transfer", options.clone()), } } diff --git a/src/plugin/ui.rs b/src/plugin/ui.rs index 0d35f3e..efe08ff 100644 --- a/src/plugin/ui.rs +++ b/src/plugin/ui.rs @@ -3,7 +3,7 @@ use crate::{ data::LoadError, ui::{ breakbar_log::BreakbarLogProps, buff_log::BuffLogProps, cast_log::CastLogProps, - multi_view::MultiViewProps, + multi_view::MultiViewProps, transfer_log::TransferLogProps, }, }; use arc_util::{ @@ -28,6 +28,7 @@ impl Plugin { cast_log, buff_log, breakbar_log, + transfer_log, .. } = &mut *Self::lock(); // for borrowing @@ -36,6 +37,7 @@ impl Plugin { cast_log.render(ui, CastLogProps { data, history }); buff_log.render(ui, BuffLogProps { history }); breakbar_log.render(ui, BreakbarLogProps { history }); + transfer_log.render(ui, TransferLogProps { history }); } } @@ -74,6 +76,12 @@ impl Plugin { "Breakbar", &mut self.breakbar_log.options.hotkey, ); + render::input_key( + ui, + "##transfer-key", + "Transfer", + &mut self.transfer_log.options.hotkey, + ); ui.spacing(); ui.spacing(); @@ -145,6 +153,7 @@ impl Plugin { ui.checkbox("Buddy Casts", plugin.cast_log.visible_mut()); ui.checkbox("Buddy Buffs", plugin.buff_log.visible_mut()); ui.checkbox("Buddy Breakbar", plugin.breakbar_log.visible_mut()); + ui.checkbox("Buddy Transfer", plugin.transfer_log.visible_mut()); } false } @@ -157,6 +166,7 @@ impl Plugin { cast_log, buff_log, breakbar_log, + transfer_log, .. } = &mut *Self::lock(); @@ -165,6 +175,7 @@ impl Plugin { && !cast_log.options.key_press(key) && !buff_log.options.key_press(key) && !breakbar_log.options.key_press(key) + && !transfer_log.options.key_press(key) } else { true } diff --git a/src/ui/mod.rs b/src/ui/mod.rs index a48db67..e90256a 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -3,6 +3,7 @@ pub mod buff_log; pub mod cast_log; pub mod multi_view; pub mod scroll; +pub mod transfer_log; // TODO: generic log component for reuse? diff --git a/src/ui/multi_view.rs b/src/ui/multi_view.rs index 2e7242a..93c9dd7 100644 --- a/src/ui/multi_view.rs +++ b/src/ui/multi_view.rs @@ -2,6 +2,7 @@ use super::{ breakbar_log::{BreakbarLog, BreakbarLogProps}, buff_log::{BuffLog, BuffLogProps}, cast_log::{CastLog, CastLogProps}, + transfer_log::{TransferLog, TransferLogProps}, }; use crate::{combat::CombatData, data::SkillData, history::History}; use arc_util::{ @@ -16,6 +17,7 @@ pub struct MultiView { pub casts: CastLog, pub buffs: BuffLog, pub breakbars: BreakbarLog, + pub transfers: TransferLog, } impl MultiView { @@ -24,6 +26,7 @@ impl MultiView { casts: CastLog::new(), buffs: BuffLog::new(), breakbars: BreakbarLog::new(), + transfers: TransferLog::new(), } } @@ -52,6 +55,9 @@ impl Component> for MultiView { Self::scroll_tab(ui, "Breakbar", || { self.breakbars.render(ui, BreakbarLogProps { history }) }); + Self::scroll_tab(ui, "Transfer", || { + self.transfers.render(ui, TransferLogProps { history }) + }); }); } } @@ -74,14 +80,17 @@ impl Windowable> for MultiView { ui.menu("Casts Display", || self.casts.render_display(ui)); ui.menu("Buffs Display", || self.buffs.render_display(ui)); ui.menu("Breakbar Display", || self.breakbars.render_display(ui)); + ui.menu("Transfer Display", || self.transfers.render_display(ui)); } } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Default, Clone, Serialize, Deserialize)] +#[serde(default)] pub struct MultiViewSettings { pub casts: ::Settings, pub buffs: ::Settings, pub breakbars: ::Settings, + pub transfers: ::Settings, } impl HasSettings for MultiView { @@ -94,6 +103,7 @@ impl HasSettings for MultiView { casts: self.casts.current_settings(), buffs: self.buffs.current_settings(), breakbars: self.breakbars.current_settings(), + transfers: self.transfers.current_settings(), } } @@ -102,9 +112,11 @@ impl HasSettings for MultiView { casts, buffs, breakbars, + transfers, } = loaded; self.casts.load_settings(casts); self.buffs.load_settings(buffs); self.breakbars.load_settings(breakbars); + self.transfers.load_settings(transfers); } } diff --git a/src/ui/transfer_log.rs b/src/ui/transfer_log.rs new file mode 100644 index 0000000..1b0cb4c --- /dev/null +++ b/src/ui/transfer_log.rs @@ -0,0 +1,112 @@ +use crate::{ + combat::CombatData, + history::History, + ui::{format_time, scroll::AutoScroll}, +}; +use arc_util::{ + colors::{GREY, RED, YELLOW}, + settings::HasSettings, + ui::{Component, Windowable}, +}; +use arcdps::{ + exports::{self, CoreColor}, + imgui::Ui, +}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(default)] +pub struct TransferLog { + display_time: bool, + + #[serde(skip)] + scroll: AutoScroll, +} + +impl TransferLog { + pub const fn new() -> Self { + Self { + display_time: true, + scroll: AutoScroll::new(), + } + } + + pub fn render_display(&mut self, ui: &Ui) { + ui.checkbox("Display time", &mut self.display_time); + } +} + +#[derive(Debug)] +pub struct TransferLogProps<'a> { + pub history: &'a mut History, +} + +impl Component> for TransferLog { + fn render(&mut self, ui: &Ui, props: TransferLogProps) { + let TransferLogProps { history } = props; + + match history.viewed_fight() { + Some(fight) if !fight.data.transfers.found().is_empty() => { + let colors = exports::colors(); + let grey = colors.core(CoreColor::MediumGrey).unwrap_or(GREY); + let red = colors.core(CoreColor::LightRed).unwrap_or(RED); + let yellow = colors.core(CoreColor::LightYellow).unwrap_or(YELLOW); + + for transfer in fight.data.transfers.found() { + if self.display_time { + ui.text_colored(grey, format_time(transfer.time)); + ui.same_line(); + } + + ui.text(transfer.stacks.to_string()); + ui.same_line(); + ui.text(transfer.condi); + + let color = if transfer.target.matches_species(fight.target) { + red + } else { + yellow + }; + ui.same_line(); + ui.text_colored(color, &transfer.target.name); + } + } + _ => ui.text("No transfers"), + } + + self.scroll.update(ui); + } +} + +impl Default for TransferLog { + fn default() -> Self { + Self::new() + } +} + +impl Windowable> for TransferLog { + const CONTEXT_MENU: bool = true; + + fn render_menu(&mut self, ui: &Ui, props: &mut TransferLogProps) { + ui.menu("History", || props.history.render_select(ui)); + + ui.spacing(); + ui.spacing(); + + ui.menu("Display", || self.render_display(ui)); + } +} + +impl HasSettings for TransferLog { + type Settings = Self; + + const SETTINGS_ID: &'static str = "transfer_log"; + + fn current_settings(&self) -> Self::Settings { + self.clone() + } + + fn load_settings(&mut self, loaded: Self::Settings) { + *self = loaded; + } +}