From 1a465ee75229d1268f20f2739ba29c4f84f70e7f Mon Sep 17 00:00:00 2001
From: Daniel Schadt <kingdread@gmx.de>
Date: Mon, 23 Apr 2018 15:14:35 +0200
Subject: 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.
---
 src/event.rs     | 350 +++++++++++++++++++++++++++++++++++++++++++++++++++++++
 src/lib.rs       | 172 +++++++++++++++++++++++++++
 src/main.rs      |  79 ++++++++++++-
 src/raw/types.rs |  25 +++-
 4 files changed, 623 insertions(+), 3 deletions(-)
 create mode 100644 src/event.rs

(limited to 'src')

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!(
-- 
cgit v1.2.3