diff options
-rw-r--r-- | src/event.rs | 350 | ||||
-rw-r--r-- | src/lib.rs | 172 | ||||
-rw-r--r-- | src/main.rs | 79 | ||||
-rw-r--r-- | src/raw/types.rs | 25 |
4 files changed, 623 insertions, 3 deletions
diff --git a/src/event.rs b/src/event.rs new file mode 100644 index 0000000..2ae3f05 --- /dev/null +++ b/src/event.rs @@ -0,0 +1,350 @@ +use super::raw; + +use num_traits::FromPrimitive; + +/// A rusty enum for all possible combat events. +/// +/// This makes dealing with `CbtEvent` a bit saner (and safer). +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum EventKind { + // State change events + /// The agent has entered combat. + EnterCombat { + agent_addr: u64, + subgroup: u64, + }, + /// The agent has left combat. + ExitCombat { + agent_addr: u64, + }, + /// The agent is now alive. + ChangeUp { + agent_addr: u64, + }, + /// The agent is now downed. + ChangeDown { + agent_addr: u64, + }, + /// The agent is now dead. + ChangeDead { + agent_addr: u64, + }, + /// The agent is now in tracking range. + Spawn { + agent_addr: u64, + }, + /// The agent has left the tracking range. + Despawn { + agent_addr: u64, + }, + /// The agent has reached a health treshold. + HealthUpdate { + agent_addr: u64, + /// The new health, as percentage multiplied by 10000. + health: u16, + }, + /// The logging has started. + LogStart { + server_timestamp: u32, + local_timestamp: u32, + }, + /// The logging has finished. + LogEnd { + server_timestamp: u32, + local_timestamp: u32, + }, + /// The agent has swapped the weapon set. + WeaponSwap { + agent_addr: u64, + set: WeaponSet, + }, + /// The given agent has its max health changed. + MaxHealthUpdate { + agent_addr: u64, + max_health: u64, + }, + /// The given agent is the point-of-view. + PointOfView { + agent_addr: u64, + }, + /// The given language is the text language. + Language { + language: raw::Language, + }, + /// The log was made with the given game build. + Build { + build: u64, + }, + /// The shard id of the server. + ShardId { + shard_id: u64, + }, + /// A reward has been awarded. + Reward { + reward_id: u64, + reward_type: i32, + }, + + /// A skill has been used. + SkillUse { + source_agent_addr: u64, + skill_id: u16, + activation: Activation, + }, + + /// Condition damage tick. + ConditionTick { + source_agent_addr: u64, + destination_agent_addr: u64, + condition_id: u16, + damage: i32, + }, + + /// Condition damage tick that was negated by invulnerability. + InvulnTick { + source_agent_addr: u64, + destination_agent_addr: u64, + condition_id: u16, + }, + + /// Physical damage. + Physical { + source_agent_addr: u64, + destination_agent_addr: u64, + skill_id: u16, + damage: i32, + result: raw::CbtResult, + }, + + /// Buff applied. + BuffApplication { + source_agent_addr: u64, + destination_agent_addr: u64, + buff_id: u16, + duration: i32, + overstack: u16, + }, + + /// Buff removed. + BuffRemove { + source_agent_addr: u64, + destination_agent_addr: u64, + buff_id: u16, + total_duration: i32, + longest_stack: i32, + removal: raw::CbtBuffRemove, + }, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Event { + pub time: u64, + pub kind: EventKind, + pub is_ninety: bool, + pub is_fifty: bool, + pub is_moving: bool, + pub is_flanking: bool, + pub is_shields: bool, +} + +impl Event { + pub fn from_raw(raw_event: &raw::CbtEvent) -> Option<Event> { + use raw::CbtStateChange; + let kind = match raw_event.is_statechange { + // Check for state change events first. + CbtStateChange::EnterCombat => EventKind::EnterCombat { + agent_addr: raw_event.src_agent, + subgroup: raw_event.dst_agent, + }, + CbtStateChange::ExitCombat => EventKind::ExitCombat { + agent_addr: raw_event.src_agent, + }, + CbtStateChange::ChangeUp => EventKind::ChangeUp { + agent_addr: raw_event.src_agent, + }, + CbtStateChange::ChangeDead => EventKind::ChangeDead { + agent_addr: raw_event.src_agent, + }, + CbtStateChange::ChangeDown => EventKind::ChangeDown { + agent_addr: raw_event.src_agent, + }, + CbtStateChange::Spawn => EventKind::Spawn { + agent_addr: raw_event.src_agent, + }, + CbtStateChange::Despawn => EventKind::Despawn { + agent_addr: raw_event.src_agent, + }, + CbtStateChange::HealthUpdate => EventKind::HealthUpdate { + agent_addr: raw_event.src_agent, + health: raw_event.dst_agent as u16, + }, + CbtStateChange::LogStart => EventKind::LogStart { + server_timestamp: raw_event.value as u32, + local_timestamp: raw_event.buff_dmg as u32, + }, + CbtStateChange::LogEnd => EventKind::LogEnd { + server_timestamp: raw_event.value as u32, + local_timestamp: raw_event.buff_dmg as u32, + }, + CbtStateChange::WeapSwap => EventKind::WeaponSwap { + agent_addr: raw_event.src_agent, + set: WeaponSet::from_u64(raw_event.dst_agent), + }, + CbtStateChange::MaxHealthUpdate => EventKind::MaxHealthUpdate { + agent_addr: raw_event.src_agent, + max_health: raw_event.dst_agent, + }, + CbtStateChange::PointOfView => EventKind::PointOfView { + agent_addr: raw_event.src_agent, + }, + CbtStateChange::Language => EventKind::Language { + language: raw::Language::from_u64(raw_event.src_agent).unwrap(), + }, + CbtStateChange::GwBuild => EventKind::Build { + build: raw_event.src_agent, + }, + CbtStateChange::ShardId => EventKind::ShardId { + shard_id: raw_event.src_agent, + }, + CbtStateChange::Reward => EventKind::Reward { + reward_id: raw_event.dst_agent, + reward_type: raw_event.value, + }, + + CbtStateChange::None => if let Some(kind) = check_activation(raw_event) { + kind + } else { + return None + }, + }; + Some(Event { + time: raw_event.time, + kind: kind, + is_ninety: raw_event.is_ninety, + is_fifty: raw_event.is_fifty, + is_moving: raw_event.is_moving, + is_flanking: raw_event.is_flanking, + is_shields: raw_event.is_shields, + }) + } +} + +fn check_activation(raw_event: &raw::CbtEvent) -> Option<EventKind> { + use raw::CbtActivation; + match raw_event.is_activation { + CbtActivation::None => check_buffremove(raw_event), + + activation => Some(EventKind::SkillUse { + source_agent_addr: raw_event.src_agent, + skill_id: raw_event.skillid, + activation: match activation { + CbtActivation::Quickness => Activation::Quickness(raw_event.value), + CbtActivation::Normal => Activation::Normal(raw_event.value), + CbtActivation::CancelFire => Activation::CancelFire(raw_event.value), + CbtActivation::CancelCancel => Activation::CancelCancel(raw_event.value), + CbtActivation::Reset => Activation::Reset, + CbtActivation::None => unreachable!(), + }, + }), + } +} + +fn check_buffremove(raw_event: &raw::CbtEvent) -> Option<EventKind> { + use raw::CbtBuffRemove; + match raw_event.is_buffremove { + CbtBuffRemove::None => check_damage(raw_event), + + removal => Some(EventKind::BuffRemove { + source_agent_addr: raw_event.src_agent, + destination_agent_addr: raw_event.dst_agent, + buff_id: raw_event.skillid, + total_duration: raw_event.value, + longest_stack: raw_event.buff_dmg, + removal, + }), + } +} + +fn check_damage(raw_event: &raw::CbtEvent) -> Option<EventKind> { + if raw_event.buff == 0 && raw_event.iff == raw::IFF::Foe && raw_event.dst_agent != 0 { + Some(EventKind::Physical { + source_agent_addr: raw_event.src_agent, + destination_agent_addr: raw_event.dst_agent, + skill_id: raw_event.skillid, + damage: raw_event.value, + result: raw_event.result, + }) + } else if raw_event.buff == 1 && raw_event.buff_dmg != 0 && raw_event.dst_agent != 0 && raw_event.value == 0 { + Some(EventKind::ConditionTick { + source_agent_addr: raw_event.src_agent, + destination_agent_addr: raw_event.dst_agent, + condition_id: raw_event.skillid, + damage: raw_event.buff_dmg, + }) + } else if raw_event.buff == 1 && raw_event.buff_dmg == 0 && raw_event.value != 0 { + Some(EventKind::BuffApplication { + source_agent_addr: raw_event.src_agent, + destination_agent_addr: raw_event.dst_agent, + buff_id: raw_event.skillid, + duration: raw_event.value, + overstack: raw_event.overstack_value, + }) + } else if raw_event.buff == 1 && raw_event.buff_dmg == 0 && raw_event.value == 0 { + Some(EventKind::InvulnTick { + source_agent_addr: raw_event.src_agent, + destination_agent_addr: raw_event.dst_agent, + condition_id: raw_event.skillid, + }) + } else { + None + } +} + +/// The different weapon-sets in game. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum WeaponSet { + /// First water weapon set. + Water0, + /// Second water weapon set. + Water1, + /// First land set. + Land0, + /// Second land set. + Land1, + /// An unknown weapon set. + /// + /// This can be caused bundles or anything else that uses the "weapon swap" + /// event but is not a normal weapon set. + Unknown(u8), +} + +impl WeaponSet { + /// Parse a given integer into the correct enum value. + pub fn from_u64(value: u64) -> WeaponSet { + match value { + // magic constants from arcdps README + 0 => WeaponSet::Water0, + 1 => WeaponSet::Water1, + 4 => WeaponSet::Land0, + 5 => WeaponSet::Land1, + _ => WeaponSet::Unknown(value as u8), + } + } +} + +/// The different types to activate a skill. +/// +/// The parameter is the animation time in milliseconds. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Activation { + /// The skill was activated with quickness. + Quickness(i32), + /// The skill was activated normally. + Normal(i32), + /// The skill was cancelled with reaching the channel time. + CancelFire(i32), + /// The skill was cancelled without reaching the channel time. + CancelCancel(i32), + /// The channel was completed successfully. + Reset, +} @@ -7,3 +7,175 @@ extern crate byteorder; extern crate num_traits; pub mod raw; + +mod event; +pub use event::{Event, EventKind}; + +quick_error! { + #[derive(Debug)] + pub enum EvtcError { + InvalidData { + description("invalid data has been provided") + } + Utf8Error(err: ::std::string::FromUtf8Error) { + from() + description("utf8 decoding error") + display("UTF-8 decoding error: {}", err) + cause(err) + } + } +} + +/// The type of an agent. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum AgentKind { + Player { profession: u32, elite: u32 }, + Gadget(u16), + Character(u16), +} + +/// Name of an agent. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum AgentName { + Single(String), + Player { character_name: String, account_name: String, subgroup: u8 } +} + +/// An agent. +#[derive(Debug, Clone)] +pub struct Agent { + addr: u64, + kind: AgentKind, + toughness: i16, + concentration: i16, + healing: i16, + condition: i16, + name: AgentName, + instance_id: u16, + first_aware: u64, + last_aware: u64, + master_agent: Option<u64>, +} + +/// A fully processed log file. +#[derive(Debug, Clone)] +pub struct Log { + agents: Vec<Agent>, +} + +pub fn process(data: &raw::Evtc) -> Result<Log, EvtcError> { + // Prepare "augmented" agents + let mut agents = setup_agents(data)?; + + // Do the first aware/last aware field + set_agent_awares(data, &mut agents)?; + + // Set the master addr field + set_agent_masters(data, &mut agents)?; + + for agent in &agents { + if let AgentKind::Player { .. } = agent.kind { + println!("Agent: {:#?}", agent); + } + } + + panic!(); +} + +fn setup_agents(data: &raw::Evtc) -> Result<Vec<Agent>, EvtcError> { + let mut agents = Vec::with_capacity(data.agents.len()); + + for raw_agent in &data.agents { + let kind = if raw_agent.is_character() { + AgentKind::Character(raw_agent.prof as u16) + } else if raw_agent.is_gadget() { + AgentKind::Gadget(raw_agent.prof as u16) + } else if raw_agent.is_player() { + AgentKind::Player { profession: raw_agent.prof, elite: raw_agent.is_elite } + } else { + return Err(EvtcError::InvalidData); + }; + + let name = if raw_agent.is_player() { + let first = raw_agent.name.iter().cloned() + .take_while(|c| *c != 0) + .collect::<Vec<_>>(); + let second = raw_agent.name.iter().cloned() + .skip(first.len() + 1) + .take_while(|c| *c != 0) + .collect::<Vec<_>>(); + let third = raw_agent.name[first.len() + second.len() + 2] - b'0'; + AgentName::Player { + character_name: String::from_utf8(first)?, + account_name: String::from_utf8(second)?, + subgroup: third, + } + } else { + let name = raw_agent.name.iter().cloned() + .take_while(|c| *c != 0) + .collect::<Vec<_>>(); + AgentName::Single(String::from_utf8(name)?) + }; + + let agent = Agent { + addr: raw_agent.addr, + kind: kind, + toughness: raw_agent.toughness, + concentration: raw_agent.concentration, + healing: raw_agent.healing, + condition: raw_agent.condition, + name: name, + instance_id: 0, + first_aware: 0, + last_aware: u64::max_value(), + master_agent: None, + }; + + agents.push(agent); + } + Ok(agents) +} + +fn get_agent_by_addr(agents: &mut [Agent], addr: u64) -> Option<&mut Agent> { + for agent in agents { + if agent.addr == addr { + return Some(agent) + } + } + None +} + +fn set_agent_awares(data: &raw::Evtc, agents: &mut [Agent]) -> Result<(), EvtcError> { + for event in &data.events { + if event.is_statechange == raw::CbtStateChange::None { + if let Some(current_agent) = get_agent_by_addr(agents, event.src_agent) { + current_agent.instance_id = event.src_instid; + if current_agent.first_aware == 0 { + current_agent.first_aware = event.time; + } + current_agent.last_aware = event.time; + } + } + } + Ok(()) +} + +fn set_agent_masters(data: &raw::Evtc, agents: &mut [Agent]) -> Result<(), EvtcError> { + for event in &data.events { + if event.src_master_instid != 0 { + let mut master_addr = None; + for agent in &*agents { + if agent.instance_id == event.src_master_instid && agent.first_aware < event.time && event.time < agent.last_aware { + master_addr = Some(agent.addr); + break; + } + } + if let Some(master_addr) = master_addr { + if let Some(current_slave) = get_agent_by_addr(agents, event.src_agent) { + current_slave.master_agent = Some(master_addr); + } + } + } + } + Ok(()) +} diff --git a/src/main.rs b/src/main.rs index 03418d0..94a8abf 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,13 +3,88 @@ extern crate evtclib; use byteorder::{ReadBytesExt, BE, LE}; use std::fs::File; -use std::io::{Seek, SeekFrom}; +use std::io::BufReader; +use std::collections::HashMap; + +// My addr: 5480786193115521456 +// My instid: 624 +// Samarog: 18003064748228432752 pub fn main() -> Result<(), evtclib::raw::parser::ParseError> { println!("Hello World!"); - let mut f = File::open("material/Samarog.evtc")?; + let mut f = BufReader::new(File::open("material/Samarog.evtc")?); let result = evtclib::raw::parse_file(&mut f)?; +/* + for agent in result.agents.iter().filter(|a| a.is_player()) { + println!("Agent: {:?}", agent); + } + + let mut damage: HashMap<u16, u64> = HashMap::new(); + let mut count = 0; + for event in result.events.iter() { + if event.is_statechange == evtclib::raw::CbtStateChange::None { + if (event.dst_agent != 0 && event.dst_instid == 0) || (event.dst_instid != 0 && event.dst_agent == 0) { + println!("{:#?}", event); + } + } + let must_take = if event.src_instid == 624 && event.skillid == 19426 && (event.value == 287 || event.buff_dmg == 287) { + println!("Event in question: {:#?}", event); + true + } else { false }; + let mut taken = false; + if event.src_instid == 624 || event.src_master_instid == 624 { + //for target in result.agents.iter().filter(|a| a.is_character()) { + if event.iff == evtclib::raw::IFF::Foe && event.dst_agent != 0 { + if event.is_statechange == evtclib::raw::CbtStateChange::None && event.is_buffremove == evtclib::raw::CbtBuffRemove::None { + let dmg = if event.buff == 1 && event.buff_dmg != 0 { + event.buff_dmg + } else if event.buff == 0 && event.value != 0 { + event.value + } else if [5, 6, 7].contains(&(event.result as u32)) { event.value } + else { + if must_take && !taken { + panic!("Failing event: {:#?}", event); + }; + continue; + }; + println!("{} {} {}", event.src_agent, event.skillid, dmg); + *damage.entry(event.skillid).or_insert(0) += dmg as u64; + count += 1; + taken = true; + } + } + //} + } + if must_take && !taken { + panic!("Failing event: {:#?}", event); + } + } + println!("Damage: {:#?}, Total: {}, Count: {}", damage, damage.values().sum::<u64>(), count); + println!("Event count: {}", result.events.len()); + println!("Events for me: {}", result.events.iter().filter(|e| e.src_instid == 624).count()); +*/ + //let processed = evtclib::process(&result); + use evtclib::EventKind; + let mut count = 0; + let mut damage = 0; + let mut failed = 0; + for event in &result.events { + let shiny = if let Some(c) = evtclib::Event::from_raw(event) { + c + } else { + println!("Failed: {:#?}", event); + failed += 1; + continue + }; + match &shiny.kind { + &EventKind::Physical { source_agent_addr: src, damage: dmg, .. } if src == 5480786193115521456 => { count += 1; damage += dmg as u64; }, + &EventKind::ConditionTick { source_agent_addr: src, damage: dmg, .. } if src == 5480786193115521456 => { count += 1; damage += dmg as u64; }, + _ => (), + } + } + println!("Count: {}, Damage: {}", count, damage); + println!("Failed events: {}", failed); Ok(()) } diff --git a/src/raw/types.rs b/src/raw/types.rs index f94c4c2..f616398 100644 --- a/src/raw/types.rs +++ b/src/raw/types.rs @@ -1,4 +1,4 @@ -use std::fmt; +use std::{self, fmt}; /// The "friend or foe" enum. #[repr(C)] @@ -234,6 +234,29 @@ pub struct Agent { pub name: [u8; 64], } +impl Agent { + /// Checks whether this agent is a gadget. + /// + /// Gadgets are entities spawned by some skills, like the "Binding Roots" + /// spawned by Entangle. + pub fn is_gadget(&self) -> bool { + self.is_elite == std::u32::MAX && (self.prof & 0xffff0000) == 0xffff0000 + } + + /// Checks whether this agent is a character. + /// + /// Characters are entities like clones, pets, minions, spirits, but also + /// minis. + pub fn is_character(&self) -> bool { + self.is_elite == std::u32::MAX && (self.prof & 0xffff0000) != 0xffff0000 + } + + /// Checks whether this agent is a player. + pub fn is_player(&self) -> bool { + self.is_elite != std::u32::MAX + } +} + impl fmt::Debug for Agent { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!( |