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.evtc Binary files differnew file mode 100644 index 0000000..f34d9c9 --- /dev/null +++ b/material/Samarog.evtc diff --git a/material/Samarog.evtc.zip b/material/Samarog.evtc.zip Binary files differnew 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() + ) + } +} |