From 6e7431f0ce600502c335b75c8acfe0cf448b68e6 Mon Sep 17 00:00:00 2001 From: Daniel Schadt Date: Sat, 14 Apr 2018 20:03:16 +0200 Subject: Initial commit --- src/raw/mod.rs | 14 +++ src/raw/parser.rs | 315 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/raw/types.rs | 289 +++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 618 insertions(+) create mode 100644 src/raw/mod.rs create mode 100644 src/raw/parser.rs create mode 100644 src/raw/types.rs (limited to 'src/raw') diff --git a/src/raw/mod.rs b/src/raw/mod.rs new file mode 100644 index 0000000..4b32955 --- /dev/null +++ b/src/raw/mod.rs @@ -0,0 +1,14 @@ +//! This module defines raw types that correspond 1:1 to the C types used in +//! [arcdps](https://www.deltaconnected.com/arcdps/evtc/README.txt). +//! +//! It is not advised to use those types and functions, as dealing with all the +//! low-level details can be quite tedious. Instead, use the higher-level +//! functions whenever possible. +mod types; + +pub use self::types::{Agent, CbtActivation, CbtBuffRemove, CbtCustomSkill, CbtEvent, CbtResult, + CbtStateChange, Language, Skill, IFF}; + +pub mod parser; + +pub use self::parser::{parse_file, Evtc, ParseError}; diff --git a/src/raw/parser.rs b/src/raw/parser.rs new file mode 100644 index 0000000..7163265 --- /dev/null +++ b/src/raw/parser.rs @@ -0,0 +1,315 @@ +//! This module contains functions to parse an EVTC file. +//! +//! # Layout +//! +//! The general layout of the EVTC file is as follows: +//! +//! ```raw +//! magic number: b'EVTC' +//! arcdps build: yyyymmdd +//! nullbyte +//! encounter id +//! nullbyte +//! agent count +//! agents +//! skill count +//! skills +//! events +//! ``` +//! +//! (refer to +//! [example.cpp](https://www.deltaconnected.com/arcdps/evtc/example.cpp) for +//! the exact data types). +//! +//! The parsing functions mirror the layout of the file and allow you to parse +//! single parts of the data (as long as your file cursor is at the right +//! position). +//! +//! All numbers are stored as little endian. +//! +//! arcdps stores the structs by just byte-dumping them. This means that you +//! have to be careful of the padding. `parse_agent` reads 96 bytes, even though +//! the struct definition only has 92. +//! +//! # Error handling +//! +//! Errors are wrapped in [`ParseError`](enum.ParseError.html). I/O errors are +//! wrapped as `ParseError::Io`. `EOF` is silently swallowed while reading the +//! events, as we expect the events to just go until the end of the file. +//! +//! Compared to the "original" enum definitions, we also add +//! [`IFF::None`](../enum.IFF.html) and +//! [`CbtResult::None`](../enum.CbtResult.html). This makes parsing easier, as +//! we can use those values instead of some other garbage. The other enums +//! already have the `None` variant, and the corresponding byte is zeroed, so +//! there's no problem with those. + +use byteorder::{LittleEndian, ReadBytesExt, LE}; +use num_traits::FromPrimitive; +use std::io::{self, ErrorKind, Read}; + +use super::*; + +/// EVTC file header. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Header { + /// arcpds build date, as `yyyymmdd` string. + pub arcdps_build: String, + /// Target species id. + pub combat_id: u16, + /// Agent count. + pub agent_count: u32, +} + +/// A completely parsed (raw) EVTC file. +#[derive(Clone, Debug)] +pub struct Evtc { + /// The file header values + pub header: Header, + /// The skill count. + pub skill_count: u32, + /// The actual agents. + pub agents: Vec, + /// The skills. + pub skills: Vec, + /// The combat events. + pub events: Vec, +} + +quick_error! { + #[derive(Debug)] + pub enum ParseError { + Io(err: io::Error) { + from() + description("io error") + display("I/O error: {}", err) + cause(err) + } + Utf8Error(err: ::std::string::FromUtf8Error) { + from() + description("utf8 decoding error") + display("UTF-8 decoding error: {}", err) + cause(err) + } + InvalidData { + from(::std::option::NoneError) + description("invalid data") + } + MalformedHeader { + description("malformed header") + } + } +} + +/// A type indicating the parse result. +type ParseResult = Result; + +/// Parse the header of an evtc file. +/// +/// It is expected that the file cursor is at the very first byte of the file. +/// +/// * `input` - Input stream. +pub fn parse_header(input: &mut T) -> ParseResult
{ + // Make sure the magic number matches + let mut magic_number = [0; 4]; + input.read_exact(&mut magic_number)?; + if &magic_number != b"EVTC" { + return Err(ParseError::MalformedHeader); + } + + // Read arcdps build date. + let mut arcdps_build = vec![0; 8]; + input.read_exact(&mut arcdps_build)?; + let build_string = String::from_utf8(arcdps_build)?; + + // Read zero delimiter + let mut zero = [0]; + input.read_exact(&mut zero)?; + if zero != [0] { + return Err(ParseError::MalformedHeader); + } + + // Read combat id. + let combat_id = input.read_u16::()?; + + // Read zero delimiter again. + input.read_exact(&mut zero)?; + if zero != [0] { + return Err(ParseError::MalformedHeader); + } + + // Read agent count. + let agent_count = input.read_u32::()?; + + Ok(Header { + arcdps_build: build_string, + combat_id: combat_id, + agent_count: agent_count, + }) +} + +/// Parse the agent array. +/// +/// This function expects the cursor to be right at the first byte of the agent +/// array. +/// +/// * `input` - Input stream. +/// * `count` - Number of agents (found in the header). +pub fn parse_agents(input: &mut T, count: u32) -> ParseResult> { + let mut result = Vec::with_capacity(count as usize); + for _ in 0..count { + result.push(parse_agent(input)?); + } + Ok(result) +} + +/// Parse a single agent. +/// +/// * `input` - Input stream. +pub fn parse_agent(input: &mut T) -> ParseResult { + let addr = input.read_u64::()?; + let prof = input.read_u32::()?; + let is_elite = input.read_u32::()?; + let toughness = input.read_i16::()?; + let concentration = input.read_i16::()?; + let healing = input.read_i16::()?; + // First padding. + input.read_i16::()?; + let condition = input.read_i16::()?; + // Second padding. + input.read_i16::()?; + let mut name = [0; 64]; + input.read_exact(&mut name)?; + + // The C structure has additional 4 bytes of padding, so that the total size + // of the struct is at 96 bytes. + // So far, we've only read 92 bytes, so we need to skip 4 more bytes. + let mut skip = [0; 4]; + input.read_exact(&mut skip)?; + + Ok(Agent { + addr: addr, + prof: prof, + is_elite: is_elite, + toughness: toughness, + concentration: concentration, + healing: healing, + condition: condition, + name: name, + }) +} + +/// Parse the skill array. +/// +/// * `input` - Input stream. +/// * `count` - Number of skills to parse. +pub fn parse_skills(input: &mut T, count: u32) -> ParseResult> { + let mut result = Vec::with_capacity(count as usize); + for _ in 0..count { + result.push(parse_skill(input)?); + } + Ok(result) +} + +/// Parse a single skill. +/// +/// * `input` - Input stream. +pub fn parse_skill(input: &mut T) -> ParseResult { + let id = input.read_i32::()?; + let mut name = [0; 64]; + input.read_exact(&mut name)?; + Ok(Skill { id: id, name: name }) +} + +/// Parse all combat events. +/// +/// * `input` - Input stream. +pub fn parse_events(input: &mut T) -> ParseResult> { + let mut result = Vec::new(); + loop { + let event = parse_event(input); + match event { + Ok(x) => result.push(x), + Err(ParseError::Io(ref e)) if e.kind() == ErrorKind::UnexpectedEof => return Ok(result), + Err(e) => return Err(e.into()), + } + } +} + +/// Parse a single combat event. +/// +/// * `input` - Input stream. +pub fn parse_event(input: &mut T) -> ParseResult { + let time = input.read_u64::()?; + let src_agent = input.read_u64::()?; + let dst_agent = input.read_u64::()?; + let value = input.read_i32::()?; + let buff_dmg = input.read_i32::()?; + let overstack_value = input.read_u16::()?; + let skillid = input.read_u16::()?; + let src_instid = input.read_u16::()?; + let dst_instid = input.read_u16::()?; + let src_master_instid = input.read_u16::()?; + + // We can skip 9 bytes of internal tracking garbage. + let mut skip = [0; 9]; + input.read_exact(&mut skip)?; + + let iff = IFF::from_u8(input.read_u8()?).unwrap_or(IFF::None); + let buff = input.read_u8()?; + let result = CbtResult::from_u8(input.read_u8()?).unwrap_or(CbtResult::None); + let is_activation = CbtActivation::from_u8(input.read_u8()?)?; + let is_buffremove = CbtBuffRemove::from_u8(input.read_u8()?)?; + let is_ninety = input.read_u8()? != 0; + let is_fifty = input.read_u8()? != 0; + let is_moving = input.read_u8()? != 0; + let is_statechange = CbtStateChange::from_u8(input.read_u8()?)?; + let is_flanking = input.read_u8()? != 0; + let is_shields = input.read_u8()? != 0; + + // Two more bytes of internal tracking garbage. + input.read_u16::()?; + + Ok(CbtEvent { + time: time, + src_agent: src_agent, + dst_agent: dst_agent, + value: value, + buff_dmg: buff_dmg, + overstack_value: overstack_value, + skillid: skillid, + src_instid: src_instid, + dst_instid: dst_instid, + src_master_instid: src_master_instid, + iff: iff, + buff: buff, + result: result, + is_activation: is_activation, + is_buffremove: is_buffremove, + is_ninety: is_ninety, + is_fifty: is_fifty, + is_moving: is_moving, + is_statechange: is_statechange, + is_flanking: is_flanking, + is_shields: is_shields, + }) +} + +/// Parse a complete EVTC file. +/// +/// * `input` - Input stream. +pub fn parse_file(input: &mut T) -> ParseResult { + let header = parse_header(input)?; + let agents = parse_agents(input, header.agent_count)?; + let skill_count = input.read_u32::()?; + let skills = parse_skills(input, skill_count)?; + let events = parse_events(input)?; + + Ok(Evtc { + header: header, + skill_count: skill_count, + agents: agents, + skills: skills, + events: events, + }) +} diff --git a/src/raw/types.rs b/src/raw/types.rs new file mode 100644 index 0000000..f94c4c2 --- /dev/null +++ b/src/raw/types.rs @@ -0,0 +1,289 @@ +use std::fmt; + +/// The "friend or foe" enum. +#[repr(C)] +#[derive(Copy, Clone, Debug, PartialEq, Eq, FromPrimitive)] +pub enum IFF { + /// Green vs green, red vs red. + Friend, + /// Green vs red. + Foe, + /// Something very wrong happened. + Unknown, + /// Field is not used in this kind of event. + None, +} + +/// Combat result (physical) +#[repr(C)] +#[derive(Copy, Clone, Debug, PartialEq, Eq, FromPrimitive)] +pub enum CbtResult { + /// Good physical hit + Normal, + /// Physical hit was a critical hit + Crit, + /// Physical hit was a glance + Glance, + /// Physical hit was blocked (e.g. Shelter) + Block, + /// Physical hit was evaded (e.g. dodge) + Evade, + /// Physical hit interrupted something + Interrupt, + /// Physical hit was absorbed (e.g. invulnerability) + Absorb, + /// Physical hit missed + Blind, + /// Physical hit was the killing blow + KillingBlow, + /// Field is not used in this kind of event. + None, +} + +/// Combat activation +#[repr(C)] +#[derive(Copy, Clone, Debug, PartialEq, Eq, FromPrimitive)] +pub enum CbtActivation { + /// Field is not used in this kind of event. + None, + /// Activation without quickness + Normal, + /// Activation with quickness + Quickness, + /// Cancel with reaching channel time + CancelFire, + /// Cancel without reaching channel time + CancelCancel, + /// Animation completed fully + Reset, +} + +/// Combat state change +/// +/// The referenced fields are of the [`CbtEvent`](struct.CbtEvent.html) +/// struct. +#[repr(C)] +#[derive(Copy, Clone, Debug, PartialEq, Eq, FromPrimitive)] +pub enum CbtStateChange { + /// Field is not used in this kind of event. + None, + /// `src_agent` entered combat. + /// + /// * `dst_agent` specifies the agent's subgroup. + EnterCombat, + /// `src_agent` left combat. + ExitCombat, + /// `src_agent` is now alive. + ChangeUp, + /// `src_agent` is now dead. + ChangeDead, + /// `src_agent` is now downed. + ChangeDown, + /// `src_agent` is now in game tracking range. + Spawn, + /// `src_agent` is no longer being tracked. + Despawn, + /// `src_agent` has reached a health marker. + /// + /// * `dst_agent` will be set to the new health percentage, multiplied by + /// 10000. + HealthUpdate, + /// Log start. + /// + /// * `value` is the server unix timestamp. + /// * `buff_dmg` is the local unix timestamp. + /// * `src_agent` is set to `0x637261` (arcdps id) + LogStart, + /// Log end. + /// + /// * `value` is the server unix timestamp. + /// * `buff_dmg` is the local unix timestamp. + /// * `src_agent` is set to `0x637261` (arcdps id) + LogEnd, + /// `src_agent` swapped the weapon set. + /// + /// * `dst_agent` is the current set id (0/1 for water sets, 4/5 for land + /// sets) + WeapSwap, + /// `src_agent` has had it's maximum health changed. + /// + /// * `dst_agent` is the new maximum health. + MaxHealthUpdate, + /// `src_agent` is the agent of the recording player. + PointOfView, + /// `src_agent` is the text language. + Language, + /// `src_agent` is the game build. + GwBuild, + /// `src_agent` is the server shard id. + ShardId, + /// Represents the reward (wiggly box) + /// + /// * `src_agent` is self + /// * `dst_agent` is reward id. + /// * `value` is reward type. + Reward, +} + +/// Combat buff remove type +#[repr(C)] +#[derive(Copy, Clone, Debug, PartialEq, Eq, FromPrimitive)] +pub enum CbtBuffRemove { + /// Field is not used in this kind of event. + None, + /// All stacks removed. + All, + /// Single stack removed. + /// + /// Disabled on server trigger, will happen for each stack on cleanse. + Single, + /// Autoremoved by OOC or allstack. + /// + /// (Ignore for strip/cleanse calc, use for in/out volume)- + Manual, +} + +/// Custom skill ids +#[repr(C)] +#[derive(Copy, Clone, Debug, PartialEq, Eq, FromPrimitive)] +pub enum CbtCustomSkill { + /// Not custom but important and unnamed. + Resurrect = 1066, + /// Personal healing only. + Bandage = 1175, + /// Will occur in is_activation==normal event. + Dodge = 65001, +} + +/// Language +#[repr(C)] +#[derive(Copy, Clone, Debug, PartialEq, Eq, FromPrimitive)] +pub enum Language { + /// English. + Eng = 0, + /// French. + Fre = 2, + /// German. + Gem = 3, + /// Spanish. + Spa = 4, +} + +/// A combat event. +#[repr(C)] +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct CbtEvent { + /// System time since Windows was started, in milliseconds. + pub time: u64, + /// Unique identifier of the source agent. + pub src_agent: u64, + /// Unique identifier of the destination agent. + pub dst_agent: u64, + /// Event-specific value. + pub value: i32, + /// Estimated buff damage. Zero on application event. + pub buff_dmg: i32, + /// Estimated overwritten stack duration for buff application. + pub overstack_value: u16, + /// Skill id. + pub skillid: u16, + /// Agent map instance id. + pub src_instid: u16, + /// Agent map instance id. + pub dst_instid: u16, + /// Master source agent map instance id if source is a minion/pet. + pub src_master_instid: u16, + pub iff: IFF, + /// Buff application, removal or damage event. + pub buff: u8, + pub result: CbtResult, + pub is_activation: CbtActivation, + pub is_buffremove: CbtBuffRemove, + /// Source agent health was over 90%. + pub is_ninety: bool, + /// Target agent health was under 90%. + pub is_fifty: bool, + /// Source agent was moving. + pub is_moving: bool, + pub is_statechange: CbtStateChange, + /// Target agent was not facing source. + pub is_flanking: bool, + /// All or part damage was vs. barrier/shield. + pub is_shields: bool, +} + +/// An agent. +#[repr(C)] +#[derive(Clone)] +pub struct Agent { + /// Agent id. + pub addr: u64, + /// Agent profession id. + pub prof: u32, + /// Agent elite specialisation. + pub is_elite: u32, + /// Toughnes. + pub toughness: i16, + /// Concentration. + pub concentration: i16, + /// Healing. + pub healing: i16, + /// Condition + pub condition: i16, + /// Name/Account combo field. + pub name: [u8; 64], +} + +impl fmt::Debug for Agent { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "Agent {{ addr: {}, \ + prof: {}, is_elite: {}, toughness: {}, concentration: {}, \ + healing: {}, condition: {}, name: {} }}", + self.addr, + self.prof, + self.is_elite, + self.toughness, + self.concentration, + self.healing, + self.condition, + String::from_utf8_lossy(&self.name) + ) + } +} + +/// A skill. +#[repr(C)] +#[derive(Clone)] +pub struct Skill { + /// Skill id. + pub id: i32, + /// Skill name. + pub name: [u8; 64], +} + +impl Skill { + /// Return the name of the skill as a `String`. + /// + /// Returns `None` if the name is not valid UTF-8. + pub fn name_string(&self) -> Option { + let bytes = self.name + .iter() + .cloned() + .take_while(|b| *b != 0) + .collect::>(); + String::from_utf8(bytes).ok() + } +} + +impl fmt::Debug for Skill { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "Skill {{ id: {}, name: {:?} }}", + self.id, + self.name_string() + ) + } +} -- cgit v1.2.3