diff --git a/Cargo.lock b/Cargo.lock index f307285..f276c5b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -71,14 +71,13 @@ dependencies = [ [[package]] name = "arcdps_buddy" -version = "0.6.4" +version = "0.6.5" dependencies = [ "arc_util", "arcdps", "log", "num_enum", "once_cell", - "phf", "proc-macro2", "quote", "semver", @@ -129,9 +128,9 @@ checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" [[package]] name = "cc" -version = "1.0.96" +version = "1.0.97" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "065a29261d53ba54260972629f9ca6bffa69bac13cd1fed61420f7fa68b9f8bd" +checksum = "099a5357d84c4c61eb35fc8eafa9a79a902c2f76911e5747ced4e032edd8d9b4" [[package]] name = "cfg-if" @@ -520,48 +519,6 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" -[[package]] -name = "phf" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" -dependencies = [ - "phf_macros", - "phf_shared", -] - -[[package]] -name = "phf_generator" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" -dependencies = [ - "phf_shared", - "rand", -] - -[[package]] -name = "phf_macros" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3444646e286606587e49f3bcf1679b8cef1dc2c5ecc29ddacaffc305180d464b" -dependencies = [ - "phf_generator", - "phf_shared", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "phf_shared" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" -dependencies = [ - "siphasher", -] - [[package]] name = "pkg-config" version = "0.3.30" @@ -595,21 +552,6 @@ dependencies = [ "proc-macro2", ] -[[package]] -name = "rand" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" -dependencies = [ - "rand_core", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" - [[package]] name = "redox_syscall" version = "0.2.16" @@ -661,11 +603,11 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "security-framework" -version = "2.10.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "770452e37cad93e0a50d5abc3990d2bc351c36d0328f86cefec2f2fb206eaef6" +checksum = "c627723fd09706bacdb5cf41499e95098555af3c3c29d014dc3c458ef6be11c0" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.5.0", "core-foundation", "core-foundation-sys", "libc", @@ -674,9 +616,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.10.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41f3cc463c0ef97e11c3461a9d3787412d30e8e7eb907c79180c4a57bf7c04ef" +checksum = "317936bbbd05227752583946b9e66d7ce3b489f84e11a94a510b4437fef407d7" dependencies = [ "core-foundation-sys", "libc", @@ -744,12 +686,6 @@ dependencies = [ "unsafe-libyaml", ] -[[package]] -name = "siphasher" -version = "0.3.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" - [[package]] name = "smallvec" version = "1.13.2" diff --git a/Cargo.toml b/Cargo.toml index 9dc160e..06b5bcd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "arcdps_buddy" -version = "0.6.4" +version = "0.6.5" edition = "2021" authors = ["Zerthox"] repository = "https://github.com/zerthox/arcdps-buddy" @@ -11,7 +11,6 @@ arcdps = { git = "https://github.com/zerthox/arcdps-rs", features = ["log", "ser log = { version = "0.4.18", features = ["release_max_level_info"] } num_enum = "0.7.1" once_cell = "1.17.2" -phf = { version = "0.11.2", features = ["macros"] } semver = { version = "1.0.17", features = ["serde"] } serde = { version = "1.0.163", features = ["derive"] } serde_yaml = "0.9.21" diff --git a/src/combat/breakbar.rs b/src/combat/breakbar.rs index 5dfb9df..6c88e82 100644 --- a/src/combat/breakbar.rs +++ b/src/combat/breakbar.rs @@ -1,4 +1,4 @@ -use super::{agent::Agent, skill::Skill}; +use super::agent::Agent; /// Information about a defiance damage hit. #[derive(Debug, Clone)] @@ -7,7 +7,7 @@ pub struct BreakbarHit { pub time: i32, /// Skill causing the hit. - pub skill: Skill, + pub skill: u32, /// Defiance damage dealt by the hit. /// @@ -28,7 +28,7 @@ impl BreakbarHit { /// Creates a new breakbar hit. pub fn new( time: i32, - skill: Skill, + skill: u32, damage: i32, attacker: Agent, is_own: bool, diff --git a/src/combat/cast.rs b/src/combat/cast.rs index 018a218..2b7acfe 100644 --- a/src/combat/cast.rs +++ b/src/combat/cast.rs @@ -1,4 +1,3 @@ -use super::skill::Skill; use arcdps::{evtc::AgentKind, Activation, Agent}; /// Information about a cast (activation). @@ -8,7 +7,7 @@ pub struct Cast { pub time: i32, /// Casted skill. - pub skill: Skill, + pub skill: u32, /// Current [`CastState`] of the cast. pub state: CastState, @@ -22,7 +21,7 @@ pub struct Cast { impl Cast { /// Creates a new cast from a cast start. - pub const fn from_start(time: i32, skill: Skill, state: CastState) -> Self { + pub const fn from_start(time: i32, skill: u32, state: CastState) -> Self { Self { time, skill, @@ -33,7 +32,7 @@ impl Cast { } /// Creates a new cast from a cast end. - pub const fn from_end(time: i32, skill: Skill, state: CastState, duration: i32) -> Self { + pub const fn from_end(time: i32, skill: u32, state: CastState, duration: i32) -> Self { Self { time, skill, @@ -44,7 +43,7 @@ impl Cast { } /// Creates a new cast from an individual hit. - pub fn from_hit(time: i32, skill: Skill, target: &Agent) -> Self { + pub fn from_hit(time: i32, skill: u32, target: &Agent) -> Self { Self { time, skill, @@ -60,7 +59,7 @@ impl Cast { } /// Completes the cast. - pub fn complete(&mut self, skill: Skill, result: CastState, duration: i32, time: i32) { + pub fn complete(&mut self, skill: u32, result: CastState, duration: i32, time: i32) { if let CastState::Pre = self.state { self.skill = skill; self.time = time - duration; diff --git a/src/combat/skill.rs b/src/combat/skill.rs index e77e303..d096410 100644 --- a/src/combat/skill.rs +++ b/src/combat/skill.rs @@ -1,29 +1,125 @@ use crate::data::SKILL_OVERRIDES; +use std::collections::{hash_map::Entry, HashMap}; + +/// Skill map keeping skill information in memory. +#[derive(Debug, Clone)] +#[repr(transparent)] +pub struct SkillMap { + map: HashMap, +} + +impl SkillMap { + /// Creates a new skill map. + pub fn new() -> Self { + Self { + map: Self::override_entries(), + } + } + + /// Creates skill override entries. + fn override_entries() -> HashMap { + SKILL_OVERRIDES + .iter() + .cloned() + .map(|(id, name)| (id, Skill::named(name))) + .collect() + } + + /// Returns the number of skill overrides. + pub fn overrides(&self) -> usize { + SKILL_OVERRIDES.len() + } + + /// Returns the number of skill entries. + pub fn len(&self) -> usize { + self.map.len() - self.overrides() + } + + /// Resets the stored skill information. + pub fn reset(&mut self) { + self.map = Self::override_entries(); + } + + /// Returns the skill information for the given id. + pub fn get(&mut self, id: u32) -> &Skill { + self.map.entry(id).or_insert_with(|| Skill::unnamed(id)) + } + + /// Returns the skill name for the given id. + pub fn get_name(&mut self, id: u32) -> &str { + self.get(id).name.as_str() + } + + /// Attempts to register a skill. + /// + /// Skills are replaced if unnamed. + fn try_register_with(&mut self, id: u32, create: impl FnOnce() -> Skill) -> &Skill { + match self.map.entry(id) { + Entry::Occupied(occupied) => { + let value = occupied.into_mut(); + if !value.is_named { + *value = create(); + } + value + } + Entry::Vacant(vacant) => vacant.insert(create()), + } + } + + /// Attempts to register a skill. + pub fn try_register(&mut self, id: u32, skill_name: Option<&str>) -> &Skill { + self.try_register_with(id, || Skill::from_combat(id, skill_name)) + } + + /// Attempts to duplicate a skill. + pub fn try_duplicate(&mut self, id: u32, from: u32) { + if id != from { + if let Some(Skill { + is_named: true, + name, + }) = self.map.get(&from) + { + let new = Skill::named(name); + self.try_register_with(id, || new); + } + } + } +} /// Information about a skill. #[derive(Debug, Clone)] pub struct Skill { - /// Id of the skill. - pub id: u32, + /// Whether the skill is named. + pub is_named: bool, /// Name of the skill. pub name: String, } impl Skill { - /// Creates a new skill. + /// Creates a new skill from combat. /// /// Name will fallback to the skill id if not present or empty. - pub fn new(id: u32, name: Option<&str>) -> Self { + fn from_combat(id: u32, skill_name: Option<&str>) -> Self { + match skill_name { + Some(name) if !name.is_empty() => Self::named(name), + _ => Self::unnamed(id), + } + } + + /// Creates a new named skill. + fn named(name: &str) -> Self { + Self { + is_named: true, + name: name.into(), + } + } + + /// Creates a new unnamed skill. + fn unnamed(id: u32) -> Self { Self { - id, - name: match SKILL_OVERRIDES.get(&id) { - Some(name) => name.to_string(), - None => match name { - Some(name) if !name.is_empty() => name.into(), - _ => id.to_string(), - }, - }, + is_named: false, + name: id.to_string(), } } } diff --git a/src/data/skill_names.rs b/src/data/skill_names.rs index 51f1c56..0a2e260 100644 --- a/src/data/skill_names.rs +++ b/src/data/skill_names.rs @@ -1,9 +1,7 @@ -use phf::phf_map; - /// Skill name overrides. -pub static SKILL_OVERRIDES: phf::Map = phf_map! { - 12815u32 => "Lightning Leap Combo", - 22492u32 => "Basilisk Venom", - 31749u32 => "Blood Moon", - 32410u32 => "Hunter's Verdict", -}; +pub static SKILL_OVERRIDES: &[(u32, &str)] = &[ + (12815, "Lightning Leap Combo"), + (22492, "Basilisk Venom"), + (31749, "Blood Moon"), + (32410, "Hunter's Verdict"), +]; diff --git a/src/lib.rs b/src/lib.rs index e1c4ce0..d22ce49 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -16,7 +16,7 @@ arcdps::export! { }, release: || Plugin::lock().unload(), combat: Plugin::area_event, - imgui: Plugin::render_windows, + imgui: Plugin::render, options_end: |ui| Plugin::lock().render_settings(ui), options_windows: Plugin::render_window_options, wnd_filter: Plugin::key_event, diff --git a/src/plugin/event.rs b/src/plugin/event.rs index fce85c9..e4891d4 100644 --- a/src/plugin/event.rs +++ b/src/plugin/event.rs @@ -4,7 +4,6 @@ use crate::combat::{ buff::{Buff, BuffApply}, cast::{Cast, CastState}, player::Player, - skill::Skill, transfer::{Apply, Condition, Remove}, }; use arcdps::{evtc::EventCategory, Activation, Agent, BuffRemove, Event, StateChange, Strike}; @@ -144,14 +143,14 @@ impl Plugin { self.history.end_latest_fight(event.time); } - pub fn latest_cast_mut(&mut self, skill: u32) -> Option<&mut Cast> { + pub fn latest_cast_mut(&mut self, id: u32) -> Option<&mut Cast> { self.history.latest_fight_mut().and_then(|fight| { fight .data .casts .iter_mut() .rev() - .find(|cast| cast.skill.id == skill) + .find(|cast| cast.skill == id) }) } @@ -168,22 +167,23 @@ impl Plugin { } fn cast_start(&mut self, event: &Event, skill_name: Option<&str>, time: i32) { - let skill = Skill::new(event.skill_id, skill_name); + let id = event.skill_id; + let skill = self.skills.try_register(id, skill_name); debug!("start {skill:?}"); - let cast = Cast::from_start(time, skill, CastState::Casting); + let cast = Cast::from_start(time, id, CastState::Casting); self.add_cast(cast); } fn cast_end(&mut self, event: &Event, skill_name: Option<&str>, time: i32) { let state = event.get_activation().into(); let duration = event.value; - - let skill = Skill::new(event.skill_id, skill_name); + let id = event.skill_id; + self.skills.try_register(id, skill_name); if let Some(cast) = self.latest_cast_mut(event.skill_id) { - cast.complete(skill, state, duration, time); + cast.complete(id, state, duration, time); debug!("complete {cast:?}"); } else { - let cast = Cast::from_end(time - duration, skill, state, duration); + let cast = Cast::from_end(time - duration, id, state, duration); debug!("complete without start {cast:?}"); self.add_cast(cast); } @@ -222,13 +222,14 @@ impl Plugin { target: &Agent, time: i32, ) { - let skill = Skill::new(event.skill_id, skill_name); + let id = event.skill_id; + self.skills.try_register(id, skill_name); let is_minion = self.is_own_minion(event); let is_own = attacker.is_self != 0 || is_minion; match event.get_strike() { Strike::Normal | Strike::Crit | Strike::Glance => { if is_own { - self.damage_hit(is_minion, skill, target, time) + self.damage_hit(is_minion, id, target, time) } } Strike::Breakbar => { @@ -236,27 +237,26 @@ impl Plugin { .get_master(event) .map(|player| player.into()) .unwrap_or(attacker.into()); - self.breakbar_hit(skill, attacker, is_own, target, event.value, time) + self.breakbar_hit(id, attacker, is_own, target, event.value, time) } _ => {} } } - fn damage_hit(&mut self, is_minion: bool, mut skill: Skill, target: &Agent, time: i32) { + fn damage_hit(&mut self, is_minion: bool, skill: u32, target: &Agent, time: i32) { // TODO: use local combat events for hits? - if let Some(info) = self.data.get(skill.id) { + if let Some(info) = self.data.get(skill) { if info.minion || !is_minion { - // replace skill id - skill.id = info.id; - let max = info.max_duration; - match self.latest_cast_mut(skill.id) { + let id = info.id; + self.skills.try_duplicate(id, skill); + match self.latest_cast_mut(id) { Some(cast) if time - cast.time <= max => { cast.hit(target); debug!("hit {:?}, {target:?}", cast.skill); } _ => { - let cast = Cast::from_hit(time, skill, target); + let cast = Cast::from_hit(time, id, target); debug!("hit without start {:?}, {target:?}", cast.skill); self.add_cast(cast); } @@ -267,7 +267,7 @@ impl Plugin { fn breakbar_hit( &mut self, - skill: Skill, + skill: u32, attacker: crate::combat::Agent, is_own: bool, target: &Agent, diff --git a/src/plugin/mod.rs b/src/plugin/mod.rs index 0542531..9414b19 100644 --- a/src/plugin/mod.rs +++ b/src/plugin/mod.rs @@ -2,7 +2,7 @@ pub mod event; pub mod ui; use crate::{ - combat::{player::Player, CombatData}, + combat::{player::Player, skill::SkillMap, CombatData}, data::{LoadError, SkillData}, history::History, ui::{ @@ -38,6 +38,8 @@ static PLUGIN: Lazy> = Lazy::new(|| Mutex::new(Plugin::new())); pub struct Plugin { updater: Updater, + skills: SkillMap, + skill_debug: bool, data: SkillData, data_state: Result, @@ -68,6 +70,8 @@ impl Plugin { VERSION.parse().unwrap(), ), + skills: SkillMap::new(), + skill_debug: false, data: SkillData::with_defaults(), data_state: Err(LoadError::NotFound), diff --git a/src/plugin/ui.rs b/src/plugin/ui.rs index efe08ff..83c0124 100644 --- a/src/plugin/ui.rs +++ b/src/plugin/ui.rs @@ -17,31 +17,47 @@ use arcdps::{ impl Plugin { /// Callback for standalone UI creation. - pub fn render_windows(ui: &Ui, not_loading: bool) { + pub fn render(ui: &Ui, not_loading: bool) { let ui_settings = exports::ui_settings(); if !ui_settings.hidden && (not_loading || ui_settings.draw_always) { - let Plugin { - updater, + Self::lock().render_windows(ui) + } + } + + /// Renders standalone UI windows. + pub fn render_windows(&mut self, ui: &Ui) { + let Plugin { + skills, + data, + history, + .. + } = self; + + self.updater.render(ui); + + self.multi_view.render( + ui, + MultiViewProps { + skills, data, history, - multi_view, - cast_log, - buff_log, - breakbar_log, - transfer_log, - .. - } = &mut *Self::lock(); // for borrowing - - updater.render(ui); - multi_view.render(ui, MultiViewProps { data, history }); - 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 }); - } + }, + ); + self.cast_log.render( + ui, + CastLogProps { + skills, + data, + history, + }, + ); + self.buff_log.render(ui, BuffLogProps { history }); + self.breakbar_log + .render(ui, BreakbarLogProps { skills, history }); + self.transfer_log.render(ui, TransferLogProps { history }); } - /// Callback for settings UI creation. + /// Renders settings UI. pub fn render_settings(&mut self, ui: &Ui) { let colors = exports::colors(); let grey = colors.core(CoreColor::MediumGrey).unwrap_or(GREY); @@ -136,16 +152,27 @@ impl Plugin { Err(LoadError::FailedToRead) => ui.text_colored(red, "Failed to read file"), Err(LoadError::Invalid) => ui.text_colored(red, "Failed to parse"), } - if ui.button("Reload") { + if ui.button("Reload##data") { self.load_data(); } ui.same_line_with_spacing(0.0, 5.0); - if ui.button("Reset") { + if ui.button("Reset##data") { self.reset_data(); } + + ui.spacing(); + ui.spacing(); + + ui.text_colored(grey, "Skill cache"); + ui.text(format!("Overrides: {}", self.skills.overrides())); + ui.text(format!("Cached: {}", self.skills.len())); + ui.checkbox("Debug##skills", &mut self.skill_debug); + if ui.button("Reset##skills") { + self.skills.reset(); + } } - /// Callback for ArcDPS option checkboxes. + /// Renders window checkboxes. pub fn render_window_options(ui: &Ui, option_name: Option<&str>) -> bool { if option_name.is_none() { let mut plugin = Self::lock(); diff --git a/src/ui/breakbar_log.rs b/src/ui/breakbar_log.rs index d662203..6a813af 100644 --- a/src/ui/breakbar_log.rs +++ b/src/ui/breakbar_log.rs @@ -1,5 +1,5 @@ use crate::{ - combat::CombatData, + combat::{skill::SkillMap, CombatData}, history::History, ui::{format_time, scroll::AutoScroll}, }; @@ -41,12 +41,13 @@ impl BreakbarLog { #[derive(Debug)] pub struct BreakbarLogProps<'a> { + pub skills: &'a mut SkillMap, pub history: &'a mut History, } impl Component> for BreakbarLog { fn render(&mut self, ui: &Ui, props: BreakbarLogProps) { - let BreakbarLogProps { history } = props; + let BreakbarLogProps { skills, history } = props; if let Some(fight) = history.viewed_fight() { let colors = exports::colors(); @@ -67,7 +68,7 @@ impl Component> for BreakbarLog { ui.text_colored(blue, format!("{}.{}", hit.damage / 10, hit.damage % 10)); ui.same_line(); - ui.text(&hit.skill.name); + ui.text(skills.get_name(hit.skill)); if self.display_others { ui.same_line(); diff --git a/src/ui/cast_log.rs b/src/ui/cast_log.rs index 3be90ed..ead995a 100644 --- a/src/ui/cast_log.rs +++ b/src/ui/cast_log.rs @@ -1,5 +1,5 @@ use crate::{ - combat::{cast::CastState, CombatData}, + combat::{cast::CastState, skill::SkillMap, CombatData}, data::{SkillData, SkillHitCount, SkillHits}, history::History, ui::{format_time, scroll::AutoScroll}, @@ -77,13 +77,18 @@ impl CastLog { #[derive(Debug)] pub struct CastLogProps<'a> { + pub skills: &'a mut SkillMap, pub data: &'a SkillData, pub history: &'a mut History, } impl Component> for CastLog { fn render(&mut self, ui: &Ui, props: CastLogProps) { - let CastLogProps { data, history } = props; + let CastLogProps { + skills, + data, + history, + } = props; if let Some(fight) = history.viewed_fight() { let colors = exports::colors(); @@ -93,7 +98,7 @@ impl Component> for CastLog { let yellow = colors.core(CoreColor::LightYellow).unwrap_or(YELLOW); for cast in &fight.data.casts { - if let Some(info) = data.get(cast.skill.id) { + if let Some(info) = data.get(cast.skill) { if self.only_misses { if let Some(hit_info) = &info.hits { if !hit_info.missed(cast.hits.len()) { @@ -107,7 +112,7 @@ impl Component> for CastLog { ui.same_line(); } - ui.text(&cast.skill.name); + ui.text(skills.get_name(cast.skill)); if let Some(hit_info) = &info.hits { if let HitDisplay::Target | HitDisplay::Both = self.display_hits { diff --git a/src/ui/multi_view.rs b/src/ui/multi_view.rs index 93c9dd7..a679852 100644 --- a/src/ui/multi_view.rs +++ b/src/ui/multi_view.rs @@ -4,7 +4,11 @@ use super::{ cast_log::{CastLog, CastLogProps}, transfer_log::{TransferLog, TransferLogProps}, }; -use crate::{combat::CombatData, data::SkillData, history::History}; +use crate::{ + combat::{skill::SkillMap, CombatData}, + data::SkillData, + history::History, +}; use arc_util::{ settings::HasSettings, ui::{Component, Windowable}, @@ -37,23 +41,36 @@ impl MultiView { #[derive(Debug)] pub struct MultiViewProps<'a> { + pub skills: &'a mut SkillMap, pub data: &'a SkillData, pub history: &'a mut History, } impl Component> for MultiView { fn render(&mut self, ui: &Ui, props: MultiViewProps) { - let MultiViewProps { data, history } = props; + let MultiViewProps { + skills, + data, + history, + } = props; TabBar::new("##tabs").build(ui, || { Self::scroll_tab(ui, "Casts", || { - self.casts.render(ui, CastLogProps { data, history }) + self.casts.render( + ui, + CastLogProps { + skills, + data, + history, + }, + ) }); Self::scroll_tab(ui, "Buffs", || { self.buffs.render(ui, BuffLogProps { history }) }); Self::scroll_tab(ui, "Breakbar", || { - self.breakbars.render(ui, BreakbarLogProps { history }) + self.breakbars + .render(ui, BreakbarLogProps { skills, history }) }); Self::scroll_tab(ui, "Transfer", || { self.transfers.render(ui, TransferLogProps { history })