aboutsummaryrefslogtreecommitdiff
path: root/src/raw/parser.rs
diff options
context:
space:
mode:
authorDaniel Schadt <kingdread@gmx.de>2018-04-14 20:03:16 +0200
committerDaniel Schadt <kingdread@gmx.de>2018-04-14 20:03:16 +0200
commit6e7431f0ce600502c335b75c8acfe0cf448b68e6 (patch)
tree1a91d65b5287d48fd14d4d1530e5cdd13f4e2ad0 /src/raw/parser.rs
downloadevtclib-6e7431f0ce600502c335b75c8acfe0cf448b68e6.tar.gz
evtclib-6e7431f0ce600502c335b75c8acfe0cf448b68e6.tar.bz2
evtclib-6e7431f0ce600502c335b75c8acfe0cf448b68e6.zip
Initial commit
Diffstat (limited to 'src/raw/parser.rs')
-rw-r--r--src/raw/parser.rs315
1 files changed, 315 insertions, 0 deletions
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,
+ })
+}