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 | |
| download | evtclib-6e7431f0ce600502c335b75c8acfe0cf448b68e6.tar.gz evtclib-6e7431f0ce600502c335b75c8acfe0cf448b68e6.tar.bz2 evtclib-6e7431f0ce600502c335b75c8acfe0cf448b68e6.zip | |
Initial commit
| -rw-r--r-- | .gitignore | 3 | ||||
| -rw-r--r-- | Cargo.toml | 10 | ||||
| -rw-r--r-- | material/README.txt | 183 | ||||
| -rw-r--r-- | material/Samarog.evtc | bin | 0 -> 13644956 bytes | |||
| -rwxr-xr-x | material/Samarog.evtc.zip | bin | 0 -> 1251759 bytes | |||
| -rw-r--r-- | material/example.cpp | 117 | ||||
| -rw-r--r-- | src/lib.rs | 9 | ||||
| -rw-r--r-- | src/main.rs | 15 | ||||
| -rw-r--r-- | src/raw/mod.rs | 14 | ||||
| -rw-r--r-- | src/raw/parser.rs | 315 | ||||
| -rw-r--r-- | src/raw/types.rs | 289 | 
11 files changed, 955 insertions, 0 deletions
| 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.evtcBinary files differ new file mode 100644 index 0000000..f34d9c9 --- /dev/null +++ b/material/Samarog.evtc diff --git a/material/Samarog.evtc.zip b/material/Samarog.evtc.zipBinary files differ new file mode 100755 index 0000000..b44a9db --- /dev/null +++ b/material/Samarog.evtc.zip 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() +        ) +    } +} | 
