aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDaniel Schadt <kingdread@gmx.de>2018-04-23 15:14:35 +0200
committerDaniel Schadt <kingdread@gmx.de>2018-04-23 15:14:35 +0200
commit1a465ee75229d1268f20f2739ba29c4f84f70e7f (patch)
tree747cb3d817aa66f150824f799b3987cbea80e877
parent6e7431f0ce600502c335b75c8acfe0cf448b68e6 (diff)
downloadevtclib-1a465ee75229d1268f20f2739ba29c4f84f70e7f.tar.gz
evtclib-1a465ee75229d1268f20f2739ba29c4f84f70e7f.tar.bz2
evtclib-1a465ee75229d1268f20f2739ba29c4f84f70e7f.zip
add basic translation to more readable events
This basically implements the "event logic" as described in the README, though it produces easier-to-digest events. The test binary show 0 failed events on an example log, but of course, not all mechanics are used there, and the parsing logic may very well contain some errors.
-rw-r--r--src/event.rs350
-rw-r--r--src/lib.rs172
-rw-r--r--src/main.rs79
-rw-r--r--src/raw/types.rs25
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,
+}
diff --git a/src/lib.rs b/src/lib.rs
index 2033ff0..aff6f2e 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -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!(