From 6e7431f0ce600502c335b75c8acfe0cf448b68e6 Mon Sep 17 00:00:00 2001
From: Daniel Schadt <kingdread@gmx.de>
Date: Sat, 14 Apr 2018 20:03:16 +0200
Subject: Initial commit

---
 .gitignore                |   3 +
 Cargo.toml                |  10 ++
 material/README.txt       | 183 +++++++++++++++++++++++++++
 material/Samarog.evtc     | Bin 0 -> 13644956 bytes
 material/Samarog.evtc.zip | Bin 0 -> 1251759 bytes
 material/example.cpp      | 117 +++++++++++++++++
 src/lib.rs                |   9 ++
 src/main.rs               |  15 +++
 src/raw/mod.rs            |  14 +++
 src/raw/parser.rs         | 315 ++++++++++++++++++++++++++++++++++++++++++++++
 src/raw/types.rs          | 289 ++++++++++++++++++++++++++++++++++++++++++
 11 files changed, 955 insertions(+)
 create mode 100644 .gitignore
 create mode 100644 Cargo.toml
 create mode 100644 material/README.txt
 create mode 100644 material/Samarog.evtc
 create mode 100755 material/Samarog.evtc.zip
 create mode 100644 material/example.cpp
 create mode 100644 src/lib.rs
 create mode 100644 src/main.rs
 create mode 100644 src/raw/mod.rs
 create mode 100644 src/raw/parser.rs
 create mode 100644 src/raw/types.rs

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..6aa1064
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,3 @@
+/target/
+**/*.rs.bk
+Cargo.lock
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644
index 0000000..7e92678
--- /dev/null
+++ b/Cargo.toml
@@ -0,0 +1,10 @@
+[package]
+name = "evtclib"
+version = "0.1.0"
+authors = ["Daniel"]
+
+[dependencies]
+num-traits = "0.2"
+num-derive = "0.2"
+quick-error = "1.2.1"
+byteorder = "1"
diff --git a/material/README.txt b/material/README.txt
new file mode 100644
index 0000000..fd0d9da
--- /dev/null
+++ b/material/README.txt
@@ -0,0 +1,183 @@
+example.cpp has the code i use to write out evtc files.
+check file header for environment: evtc bytes for filetype, arcdps build yyyymmdd for compatibility, target species id for boss/manual.
+create agents and skills table from evtc, consider dynamic expansion - some nameless/combatless agents may not be written.
+skill and agent names use utf8 encoding.
+evtc_agent.name is a combo string on players - character name <null> account name <null> subgroup str literal <null>.
+add u16 field to the agent table, agents[x].instance_id, initialized to 0.
+add u64 fields agents[x].first_aware initialized to 0, and agents[x].last_aware initialized to u64max.
+add u64 field agents[x].master_addr, initialized to 0.
+if evtc_agent.is_elite == 0xffffffff && upper half of evtc_agent.prof == 0xffff, agent is a gadget with pseudo id as lower half of evtc_agent.prof (volatile id).
+if evtc_agent.is_elite == 0xffffffff && upper half of evtc_agent.prof != 0xffff, agent is a character with species id as lower half of evtc_agent.prof (reliable id).
+if evtc_agent.is_elite != 0xffffffff, agent is a player with profession as evtc_agent.prof and elite spec as evtc_agent.is_elite.
+gadgets do not have true ids and are generated through a combination of gadget parameters - they may collide with characters and should be treated separately.
+iterate through all events, assigning instance ids and first/last aware ticks.
+set agents[x].instance_id = cbtevent.src_instid where agents[x].addr == cbtevent.src_agent && !cbtevent.is_statechange.
+set agents[x].first_aware = cbtevent.time on first event, then all consecutive event times to agents[x].last_aware.
+iterate through all events again, this time assigning master agent.
+set agents[z].master_agent on encountering cbtevent.src_master_instid != 0.
+agents[z].master_addr = agents[x].addr where agents[x].instance_id == cbtevent.src_master_instid && agent[x].first_aware < cbtevent.time < last_aware.
+iterate through all events one last time, this time parsing for the data you want.
+cbtevent.src_agent and cbtevent.dst_agent should be used to associate event data with local data.
+parse event type order should check cbtevent.is_statechange > cbtevent.is_activation > cbtevent.is_buffremove.
+cbtevent.is_statechange will do the heavy lifting of non-internal and parser-requested events - make sure to ignore unknown statechange types.
+if you would like to see an event not obtainable through my cbtevent struct, let me know.
+on cbtevent.is_activation == cancel_fire or cancel_cancel, value will be the ms duration of the time spent in animation.
+on cbtevent.is_activation == normal or quickness, value will be the ms duration of the expected animation time.
+on cbtevent.is_buffremove, value will be the total duration removed of the stack/s, buff_dmg will be the duration of the longest stack.
+if they are all 0, it will be a buff application (!cbtevent.is_buffremove && cbtevent.is_buff) or physical hit (!cbtevent.is_buff).
+on physical, cbtevent.value will be the damage done (negative = healing, if ever added).
+on physical, cbtevent.result will be the result of the attack.
+on buff && !cbtevent.buff_dmg && cbtevent.value, cbtevent.value will be the millisecond duration applied. cbtevent.overstack will be apx overstack in ms.
+on buff && !cbtevent.buff_dmg && !cbtevent.value, it is negated condition damage via invuln or resistance.
+on buff && cbtevent.buff_dmg && !cbtevent.value, cbtevent.buff_dmg will be the approximate damage done on tick (negative = healing, if ever added).
+
+raid boss ids:
+vale guardian  = 0x3C4E
+gorseval       = 0x3C45
+sabetha        = 0x3C0F
+slothasor      = 0x3EFB
+trio           = 0x3ED8, 0x3F09, 0x3EFD
+matthias       = 0x3EF3
+stk            = 
+keep construct = 0x3F6B
+xera           = 0x3F76, 0x3F9E
+cairn          = 0x432A
+overseer       = 0x4314
+samarog        = 0x4324
+deimos         = 0x4302
+some fractal ids (thanks /u/hollywood_rag):
+https://pastebin.com/3GpfDfGe
+
+base npc stats (for levels 0 to 84):
+def = 
+{  123,  128,  134,  138,  143,  148,  153,  158,  162,  167,  175,  183,  185,  187,  190,  192,  202,  206,  210,  214,
+   220,  224,  239,  245,  250,  256,  261,  267,  285,  291,  311,  320,  328,  337,  356,  365,  385,  394,  402,  411,
+   432,  443,  465,  476,  486,  497,  517,  527,  550,  561,  575,  588,  610,  624,  649,  662,  676,  690,  711,  725,
+   752,  769,  784,  799,  822,  837,  878,  893,  909,  924,  949,  968, 1011, 1030, 1049, 1067, 1090, 1109, 1155, 1174,
+  1223, 1247, 1271, 1295, 1319}
+  
+pwr = 
+{  162,  179,  197,  214,  231,  249,  267,  286,  303,  322,  344,  367,  389,  394,  402,  412,  439,  454,  469,  483,
+   500,  517,  556,  575,  593,  612,  622,  632,  672,  684,  728,  744,  761,  778,  820,  839,  885,  905,  924,  943,
+   991, 1016, 1067, 1093, 1119, 1145, 1193, 1220, 1275, 1304, 1337, 1372, 1427, 1461, 1525, 1562, 1599, 1637, 1692, 1731,
+  1802, 1848, 1891, 1936, 1999, 2045, 2153, 2201, 2249, 2298, 2368, 2424, 2545, 2604, 2662, 2723, 2792, 2854, 2985, 3047,
+  3191, 3269, 3348, 3427, 3508}
+  
+attr = 
+{    5,   10,   17,   22,   27,   35,   45,   50,   55,   60,   68,   76,   84,   92,   94,   95,  103,  108,  112,  116,
+   123,  129,  140,  147,  153,  160,  166,  171,  186,  192,  208,  219,  230,  238,  253,  259,  274,  279,  284,  290,
+   304,  317,  339,  353,  366,  380,  401,  416,  440,  454,  471,  488,  514,  532,  561,  579,  598,  617,  643,  662,
+   696,  718,  741,  765,  795,  818,  866,  891,  916,  941,  976, 1004, 1059, 1089, 1119, 1149, 1183, 1214, 1274, 1307,
+  1374, 1413, 1453, 1493, 1534}
+
+enums and structs: 
+/* iff */
+enum iff {
+	IFF_FRIEND, // green vs green, red vs red
+	IFF_FOE, // green vs red
+	IFF_UNKNOWN // something very wrong happened
+};
+
+/* combat result (physical) */
+enum cbtresult {
+	CBTR_NORMAL, // good physical hit
+	CBTR_CRIT, // physical hit was crit
+	CBTR_GLANCE, // physical hit was glance
+	CBTR_BLOCK, // physical hit was blocked eg. mesmer shield 4
+	CBTR_EVADE, // physical hit was evaded, eg. dodge or mesmer sword 2
+	CBTR_INTERRUPT, // physical hit interrupted something
+	CBTR_ABSORB, // physical hit was "invlun" or absorbed eg. guardian elite
+	CBTR_BLIND, // physical hit missed
+	CBTR_KILLINGBLOW // physical hit was killing hit
+};
+	
+/* combat activation */
+enum cbtactivation {
+	ACTV_NONE, // not used - not this kind of event
+	ACTV_NORMAL, // activation without quickness
+	ACTV_QUICKNESS, // activation with quickness
+	ACTV_CANCEL_FIRE, // cancel with reaching channel time
+	ACTV_CANCEL_CANCEL, // cancel without reaching channel time
+	ACTV_RESET // animation completed fully
+};
+
+/* combat state change */
+enum cbtstatechange {
+	CBTS_NONE, // not used - not this kind of event
+	CBTS_ENTERCOMBAT, // src_agent entered combat, dst_agent is subgroup
+	CBTS_EXITCOMBAT, // src_agent left combat
+	CBTS_CHANGEUP, // src_agent is now alive
+	CBTS_CHANGEDEAD, // src_agent is now dead
+	CBTS_CHANGEDOWN, // src_agent is now downed
+	CBTS_SPAWN, // src_agent is now in game tracking range
+	CBTS_DESPAWN, // src_agent is no longer being tracked
+	CBTS_HEALTHUPDATE, // src_agent has reached a health marker. dst_agent = percent * 10000 (eg. 99.5% will be 9950)
+	CBTS_LOGSTART, // log start. value = server unix timestamp **uint32**. buff_dmg = local unix timestamp. src_agent = 0x637261 (arcdps id)
+	CBTS_LOGEND, // log end. value = server unix timestamp **uint32**. buff_dmg = local unix timestamp. src_agent = 0x637261 (arcdps id)
+	CBTS_WEAPSWAP, // src_agent swapped weapon set. dst_agent = current set id (0/1 water, 4/5 land)
+	CBTS_MAXHEALTHUPDATE, // src_agent has had it's maximum health changed. dst_agent = new max health
+	CBTS_POINTOFVIEW, // src_agent will be agent of "recording" player
+	CBTS_LANGUAGE, // src_agent will be text language
+	CBTS_GWBUILD, // src_agent will be game build
+	CBTS_SHARDID, // src_agent will be sever shard id
+	CBTS_REWARD // src_agent is self, dst_agent is reward id, value is reward type. these are the wiggly boxes that you get
+};
+
+/* combat buff remove type */
+enum cbtbuffremove {
+	CBTB_NONE, // not used - not this kind of event
+	CBTB_ALL, // all stacks removed
+	CBTB_SINGLE, // single stack removed. disabled on server trigger, will happen for each stack on cleanse
+	CBTB_MANUAL, // autoremoved by ooc or allstack (ignore for strip/cleanse calc, use for in/out volume)
+};
+
+/* custom skill ids */
+enum cbtcustomskill {
+	CSK_RESURRECT = 1066, // not custom but important and unnamed
+	CSK_BANDAGE = 1175, // personal healing only
+	CSK_DODGE = 65001 // will occur in is_activation==normal event
+};
+
+/* language */
+enum gwlanguage {
+	GWL_ENG = 0,
+	GWL_FRE = 2,
+	GWL_GEM = 3,
+	GWL_SPA = 4,
+};
+
+/* combat event */
+typedef struct cbtevent {
+	uint64_t time; /* timegettime() at time of event */
+	uint64_t src_agent; /* unique identifier */
+	uint64_t dst_agent; /* unique identifier */
+	int32_t value; /* event-specific */
+	int32_t buff_dmg; /* estimated buff damage. zero on application event */
+	uint16_t overstack_value; /* estimated overwritten stack duration for buff application */
+	uint16_t skillid; /* skill id */
+	uint16_t src_instid; /* agent map instance id */
+	uint16_t dst_instid; /* agent map instance id */
+	uint16_t src_master_instid; /* master source agent map instance id if source is a minion/pet */
+	uint8_t iss_offset; /* internal tracking. garbage */
+	uint8_t iss_offset_target; /* internal tracking. garbage */
+	uint8_t iss_bd_offset; /* internal tracking. garbage */
+	uint8_t iss_bd_offset_target; /* internal tracking. garbage */
+	uint8_t iss_alt_offset; /* internal tracking. garbage */
+	uint8_t iss_alt_offset_target; /* internal tracking. garbage */
+	uint8_t skar; /* internal tracking. garbage */
+	uint8_t skar_alt; /* internal tracking. garbage */
+	uint8_t skar_use_alt; /* internal tracking. garbage */
+	uint8_t iff; /* from iff enum */
+	uint8_t buff; /* buff application, removal, or damage event */
+	uint8_t result; /* from cbtresult enum */
+	uint8_t is_activation; /* from cbtactivation enum */
+	uint8_t is_buffremove; /* buff removed. src=relevant, dst=caused it (for strips/cleanses). from cbtr enum */
+	uint8_t is_ninety; /* source agent health was over 90% */
+	uint8_t is_fifty; /* target agent health was under 50% */
+	uint8_t is_moving; /* source agent was moving */
+	uint8_t is_statechange; /* from cbtstatechange enum */
+	uint8_t is_flanking; /* target agent was not facing source */
+	uint8_t is_shields; /* all or part damage was vs barrier/shield */
+	uint8_t result_local; /* internal tracking. garbage */
+	uint8_t ident_local; /* internal tracking. garbage */
+} cbtevent;
diff --git a/material/Samarog.evtc b/material/Samarog.evtc
new file mode 100644
index 0000000..f34d9c9
Binary files /dev/null and b/material/Samarog.evtc differ
diff --git a/material/Samarog.evtc.zip b/material/Samarog.evtc.zip
new file mode 100755
index 0000000..b44a9db
Binary files /dev/null and b/material/Samarog.evtc.zip differ
diff --git a/material/example.cpp b/material/example.cpp
new file mode 100644
index 0000000..21fcc65
--- /dev/null
+++ b/material/example.cpp
@@ -0,0 +1,117 @@
+/* write event chain */
+uint32_t writeencounter(FILE* fd, AList* al_combat, AList* al_agents, uint32_t start_type) {
+	/* file byte index */
+	uint32_t fdindex = 0;
+
+	/* write header (16 bytes) */
+	char header[32];
+	asnprintf(&header[0], 32, "EVTC%s", g->m_version);
+	header[12] = 0;
+	*(uint16_t*)(&header[13]) = g->m_game->m_area_cbt_cid;
+	header[15] = 0;
+	fseek(fd, 0, SEEK_SET);
+	fwrite(&header[0], 16, 1, fd);
+	fdindex += 16;
+
+	/* define agent. stats range from 0-10 */
+	typedef struct evtc_agent {
+		uint64_t addr;
+		uint32_t prof;
+		uint32_t is_elite;
+		int16_t toughness;
+		int16_t concentration;
+		int16_t healing;
+		int16_t pad1;
+		int16_t condition;
+		int16_t pad2;
+		char name[64];
+	} evtc_agent;
+
+	/* count agents */
+	alisti itr;
+	uint32_t ag_count = 0;
+	int32_t max_toughness = 1;
+	int32_t max_healing = 1;
+	int32_t max_condition = 1;
+	al_agents->IInitTail(&itr);
+	evtc_agent* evag = (evtc_agent*)al_agents->INext(&itr);
+	while (evag) {
+		ag_count += 1;
+		max_toughness = MAX(evag->toughness, max_toughness);
+		max_healing = MAX(evag->healing, max_healing);
+		max_condition = MAX(evag->condition, max_condition);
+		evag = (evtc_agent*)al_agents->INext(&itr);
+	}
+
+	/* write agent count */
+	fseek(fd, fdindex, SEEK_SET);
+	fwrite(&ag_count, sizeof(uint32_t), 1, fd);
+	fdindex += sizeof(uint32_t);
+
+	/* write agent array */
+	al_agents->IInitTail(&itr);
+	evag = (evtc_agent*)al_agents->INext(&itr);
+	while (evag) {
+		evag->toughness = ((evag->toughness * 100) / max_toughness) / 10;
+		evag->healing = ((evag->healing * 100) / max_healing) / 10;
+		evag->condition = ((evag->condition * 100) / max_condition) / 10;
+		fseek(fd, fdindex, SEEK_SET);
+		fwrite(evag, sizeof(evtc_agent), 1, fd);
+		fdindex += sizeof(evtc_agent);
+		evag = (evtc_agent*)al_agents->INext(&itr);
+	}
+
+	/* count skills */
+	uint8_t* sk_mask = (uint8_t*)acalloc(sizeof(uint8_t) * 65535);
+	al_combat->IInitTail(&itr);
+	cbtevent* cbtev = (cbtevent*)al_combat->INext(&itr);
+	uint32_t skcount = 0;
+	while (cbtev) {
+		if (!sk_mask[cbtev->skillid]) {
+			skcount += 1;
+			sk_mask[cbtev->skillid] = 1;
+		}
+		cbtev = (cbtevent*)al_combat->INext(&itr);
+	}
+
+	/* write skill count */
+	fseek(fd, fdindex, SEEK_SET);
+	fwrite(&skcount, sizeof(uint32_t), 1, fd);
+	fdindex += sizeof(uint32_t);
+
+	/* define skill */
+	typedef struct skill {
+		int32_t id;
+		char name[64];
+	} skill;
+
+	/* write skill array */
+	skcount = 0;
+	while (skcount < 65535) {
+		if (sk_mask[skcount]) {
+			skill temp;
+			memset(&temp, 0, sizeof(skill));
+			temp.id = g->m_game->m_ar_sks[skcount].skillid;
+			asnprintf(&temp.name[0], RB_NAME_LEN, "%s", g->m_game->m_ar_sks[skcount].name);
+			fseek(fd, fdindex, SEEK_SET);
+			fwrite(&temp, sizeof(skill), 1, fd);
+			fdindex += sizeof(skill);
+		}
+		skcount += 1;
+	}
+	acfree(sk_mask);
+
+	/* write combat log */
+	al_combat->IInitTail(&itr);
+	cbtev = (cbtevent*)al_combat->INext(&itr);
+	while (cbtev) {
+		fseek(fd, fdindex, SEEK_SET);
+		fwrite(cbtev, sizeof(cbtevent), 1, fd);
+		fdindex += sizeof(cbtevent);
+		cbtev = (cbtevent*)al_combat->INext(&itr);
+	}
+
+	/* cleanup */
+	fclose(fd);
+	return fdindex;
+}
diff --git a/src/lib.rs b/src/lib.rs
new file mode 100644
index 0000000..2033ff0
--- /dev/null
+++ b/src/lib.rs
@@ -0,0 +1,9 @@
+#![feature(try_trait)]
+#[macro_use]
+extern crate quick_error;
+#[macro_use]
+extern crate num_derive;
+extern crate byteorder;
+extern crate num_traits;
+
+pub mod raw;
diff --git a/src/main.rs b/src/main.rs
new file mode 100644
index 0000000..03418d0
--- /dev/null
+++ b/src/main.rs
@@ -0,0 +1,15 @@
+extern crate byteorder;
+extern crate evtclib;
+use byteorder::{ReadBytesExt, BE, LE};
+use std::fs::File;
+
+use std::io::{Seek, SeekFrom};
+
+pub fn main() -> Result<(), evtclib::raw::parser::ParseError> {
+    println!("Hello World!");
+    let mut f = File::open("material/Samarog.evtc")?;
+
+    let result = evtclib::raw::parse_file(&mut f)?;
+
+    Ok(())
+}
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()
+        )
+    }
+}
-- 
cgit v1.2.3