aboutsummaryrefslogtreecommitdiff
path: root/src/raw
diff options
context:
space:
mode:
Diffstat (limited to 'src/raw')
-rw-r--r--src/raw/mod.rs14
-rw-r--r--src/raw/parser.rs315
-rw-r--r--src/raw/types.rs289
3 files changed, 618 insertions, 0 deletions
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<Agent>,
+ /// The skills.
+ pub skills: Vec<Skill>,
+ /// The combat events.
+ pub events: Vec<CbtEvent>,
+}
+
+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<T> = Result<T, ParseError>;
+
+/// 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<T: Read>(input: &mut T) -> ParseResult<Header> {
+ // 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::<LittleEndian>()?;
+
+ // 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::<LittleEndian>()?;
+
+ 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<T: Read>(input: &mut T, count: u32) -> ParseResult<Vec<Agent>> {
+ 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<T: Read>(input: &mut T) -> ParseResult<Agent> {
+ let addr = input.read_u64::<LittleEndian>()?;
+ let prof = input.read_u32::<LittleEndian>()?;
+ let is_elite = input.read_u32::<LittleEndian>()?;
+ let toughness = input.read_i16::<LittleEndian>()?;
+ let concentration = input.read_i16::<LittleEndian>()?;
+ let healing = input.read_i16::<LittleEndian>()?;
+ // First padding.
+ input.read_i16::<LittleEndian>()?;
+ let condition = input.read_i16::<LittleEndian>()?;
+ // Second padding.
+ input.read_i16::<LittleEndian>()?;
+ 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<T: Read>(input: &mut T, count: u32) -> ParseResult<Vec<Skill>> {
+ 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<T: Read>(input: &mut T) -> ParseResult<Skill> {
+ let id = input.read_i32::<LittleEndian>()?;
+ 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<T: Read>(input: &mut T) -> ParseResult<Vec<CbtEvent>> {
+ 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<T: Read>(input: &mut T) -> ParseResult<CbtEvent> {
+ let time = input.read_u64::<LittleEndian>()?;
+ let src_agent = input.read_u64::<LE>()?;
+ let dst_agent = input.read_u64::<LE>()?;
+ let value = input.read_i32::<LE>()?;
+ let buff_dmg = input.read_i32::<LE>()?;
+ let overstack_value = input.read_u16::<LE>()?;
+ let skillid = input.read_u16::<LE>()?;
+ let src_instid = input.read_u16::<LE>()?;
+ let dst_instid = input.read_u16::<LE>()?;
+ let src_master_instid = input.read_u16::<LE>()?;
+
+ // 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::<LE>()?;
+
+ 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<T: Read>(input: &mut T) -> ParseResult<Evtc> {
+ let header = parse_header(input)?;
+ let agents = parse_agents(input, header.agent_count)?;
+ let skill_count = input.read_u32::<LittleEndian>()?;
+ 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<String> {
+ let bytes = self.name
+ .iter()
+ .cloned()
+ .take_while(|b| *b != 0)
+ .collect::<Vec<_>>();
+ 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()
+ )
+ }
+}