diff options
author | Daniel Schadt <kingdread@gmx.de> | 2018-04-14 20:03:16 +0200 |
---|---|---|
committer | Daniel Schadt <kingdread@gmx.de> | 2018-04-14 20:03:16 +0200 |
commit | 6e7431f0ce600502c335b75c8acfe0cf448b68e6 (patch) | |
tree | 1a91d65b5287d48fd14d4d1530e5cdd13f4e2ad0 /src/raw/parser.rs | |
download | evtclib-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.rs | 315 |
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, + }) +} |