use std::collections::HashMap;
use std::convert::TryFrom;
use std::marker::PhantomData;
use getset::{CopyGetters, Getters};
use num_traits::FromPrimitive;
use thiserror::Error;
pub mod raw;
pub mod event;
pub use event::{Event, EventKind};
mod processing;
pub use processing::{process, process_file, process_stream, Compression};
pub mod gamedata;
use gamedata::CmTrigger;
pub use gamedata::{Boss, EliteSpec, Profession};
#[derive(Error, Debug)]
pub enum EvtcError {
#[error("the file could not be parsed: {0}")]
ParseError(#[from] raw::ParseError),
#[error("invalid data has been provided")]
InvalidData,
#[error("invalid profession id: {0}")]
InvalidProfession(u32),
#[error("invalid elite specialization id: {0}")]
InvalidEliteSpec(u32),
#[error("utf8 decoding error: {0}")]
Utf8Error(#[from] std::str::Utf8Error),
}
#[derive(Debug, Clone, Hash, PartialEq, Eq, CopyGetters)]
pub struct Player {
#[get_copy = "pub"]
profession: Profession,
#[get_copy = "pub"]
elite: Option<EliteSpec>,
character_name: String,
account_name: String,
#[get_copy = "pub"]
subgroup: u8,
}
impl Player {
pub fn character_name(&self) -> &str {
&self.character_name
}
pub fn account_name(&self) -> &str {
&self.account_name
}
}
#[derive(Debug, Clone, Hash, PartialEq, Eq, CopyGetters)]
pub struct Gadget {
#[get_copy = "pub"]
id: u16,
name: String,
}
impl Gadget {
pub fn name(&self) -> &str {
&self.name
}
}
#[derive(Debug, Clone, Hash, PartialEq, Eq, CopyGetters)]
pub struct Character {
#[get_copy = "pub"]
id: u16,
name: String,
}
impl Character {
pub fn name(&self) -> &str {
&self.name
}
}
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
pub enum AgentKind {
Player(Player),
Gadget(Gadget),
Character(Character),
}
impl AgentKind {
fn from_raw_character(raw_agent: &raw::Agent) -> Result<Character, EvtcError> {
assert!(raw_agent.is_character());
let name = raw::cstr_up_to_nul(&raw_agent.name).ok_or(EvtcError::InvalidData)?;
Ok(Character {
id: raw_agent.prof as u16,
name: name.to_str()?.to_owned(),
})
}
fn from_raw_gadget(raw_agent: &raw::Agent) -> Result<Gadget, EvtcError> {
assert!(raw_agent.is_gadget());
let name = raw::cstr_up_to_nul(&raw_agent.name).ok_or(EvtcError::InvalidData)?;
Ok(Gadget {
id: raw_agent.prof as u16,
name: name.to_str()?.to_owned(),
})
}
fn from_raw_player(raw_agent: &raw::Agent) -> Result<Player, EvtcError> {
assert!(raw_agent.is_player());
let character_name = raw::cstr_up_to_nul(&raw_agent.name)
.ok_or(EvtcError::InvalidData)?
.to_str()?;
let account_name = raw::cstr_up_to_nul(&raw_agent.name[character_name.len() + 1..])
.ok_or(EvtcError::InvalidData)?
.to_str()?;
let subgroup = raw_agent.name[character_name.len() + account_name.len() + 2] - b'0';
let elite = if raw_agent.is_elite == 0 {
None
} else {
Some(
EliteSpec::from_u32(raw_agent.is_elite)
.ok_or(EvtcError::InvalidEliteSpec(raw_agent.is_elite))?,
)
};
Ok(Player {
profession: Profession::from_u32(raw_agent.prof)
.ok_or(EvtcError::InvalidProfession(raw_agent.prof))?,
elite,
character_name: character_name.to_owned(),
account_name: account_name.to_owned(),
subgroup,
})
}
pub fn as_player(&self) -> Option<&Player> {
if let AgentKind::Player(ref player) = *self {
Some(player)
} else {
None
}
}
pub fn is_player(&self) -> bool {
self.as_player().is_some()
}
pub fn as_gadget(&self) -> Option<&Gadget> {
if let AgentKind::Gadget(ref gadget) = *self {
Some(gadget)
} else {
None
}
}
pub fn is_gadget(&self) -> bool {
self.as_gadget().is_some()
}
pub fn as_character(&self) -> Option<&Character> {
if let AgentKind::Character(ref character) = *self {
Some(character)
} else {
None
}
}
pub fn is_character(&self) -> bool {
self.as_character().is_some()
}
}
impl TryFrom<raw::Agent> for AgentKind {
type Error = EvtcError;
fn try_from(raw_agent: raw::Agent) -> Result<Self, Self::Error> {
Self::try_from(&raw_agent)
}
}
impl TryFrom<&raw::Agent> for AgentKind {
type Error = EvtcError;
fn try_from(raw_agent: &raw::Agent) -> Result<Self, Self::Error> {
if raw_agent.is_character() {
Ok(AgentKind::Character(AgentKind::from_raw_character(
raw_agent,
)?))
} else if raw_agent.is_gadget() {
Ok(AgentKind::Gadget(AgentKind::from_raw_gadget(raw_agent)?))
} else if raw_agent.is_player() {
Ok(AgentKind::Player(AgentKind::from_raw_player(raw_agent)?))
} else {
Err(EvtcError::InvalidData)
}
}
}
#[derive(Debug, Clone, Hash, PartialEq, Eq, Getters, CopyGetters)]
#[repr(C)]
pub struct Agent<Kind = ()> {
#[get_copy = "pub"]
addr: u64,
#[get = "pub"]
kind: AgentKind,
#[get_copy = "pub"]
toughness: i16,
#[get_copy = "pub"]
concentration: i16,
#[get_copy = "pub"]
healing: i16,
#[get_copy = "pub"]
condition: i16,
#[get_copy = "pub"]
instance_id: u16,
#[get_copy = "pub"]
first_aware: u64,
#[get_copy = "pub"]
last_aware: u64,
#[get_copy = "pub"]
master_agent: Option<u64>,
phantom_data: PhantomData<Kind>,
}
impl TryFrom<&raw::Agent> for Agent {
type Error = EvtcError;
fn try_from(raw_agent: &raw::Agent) -> Result<Self, Self::Error> {
let kind = AgentKind::try_from(raw_agent)?;
Ok(Agent {
addr: raw_agent.addr,
kind,
toughness: raw_agent.toughness,
concentration: raw_agent.concentration,
healing: raw_agent.healing,
condition: raw_agent.condition,
instance_id: 0,
first_aware: 0,
last_aware: u64::max_value(),
master_agent: None,
phantom_data: PhantomData,
})
}
}
impl TryFrom<raw::Agent> for Agent {
type Error = EvtcError;
fn try_from(raw_agent: raw::Agent) -> Result<Self, Self::Error> {
Agent::try_from(&raw_agent)
}
}
impl<Kind> Agent<Kind> {
#[inline]
fn transmute<T>(&self) -> &Agent<T> {
unsafe { &*(self as *const Agent<Kind> as *const Agent<T>) }
}
#[inline]
pub fn erase(&self) -> &Agent {
self.transmute()
}
#[inline]
pub fn as_player(&self) -> Option<&Agent<Player>> {
if self.kind.is_player() {
Some(self.transmute())
} else {
None
}
}
#[inline]
pub fn as_gadget(&self) -> Option<&Agent<Gadget>> {
if self.kind.is_gadget() {
Some(self.transmute())
} else {
None
}
}
#[inline]
pub fn as_character(&self) -> Option<&Agent<Character>> {
if self.kind.is_character() {
Some(self.transmute())
} else {
None
}
}
}
impl Agent<Player> {
#[inline]
pub fn player(&self) -> &Player {
self.kind.as_player().expect("Agent<Player> had no player!")
}
#[inline]
pub fn account_name(&self) -> &str {
self.player().account_name()
}
#[inline]
pub fn character_name(&self) -> &str {
self.player().character_name()
}
#[inline]
pub fn elite(&self) -> Option<EliteSpec> {
self.player().elite()
}
#[inline]
pub fn profession(&self) -> Profession {
self.player().profession()
}
#[inline]
pub fn subgroup(&self) -> u8 {
self.player().subgroup()
}
}
impl Agent<Gadget> {
#[inline]
pub fn gadget(&self) -> &Gadget {
self.kind.as_gadget().expect("Agent<Gadget> had no gadget!")
}
#[inline]
pub fn id(&self) -> u16 {
self.gadget().id()
}
#[inline]
pub fn name(&self) -> &str {
self.gadget().name()
}
}
impl Agent<Character> {
#[inline]
pub fn character(&self) -> &Character {
self.kind
.as_character()
.expect("Agent<Character> had no character!")
}
#[inline]
pub fn id(&self) -> u16 {
self.character().id()
}
#[inline]
pub fn name(&self) -> &str {
self.character().name()
}
}
#[derive(Debug, Clone)]
pub struct Log {
agents: Vec<Agent>,
events: Vec<Event>,
boss_id: u16,
}
impl Log {
#[inline]
pub fn agents(&self) -> &[Agent] {
&self.agents
}
pub fn agent_by_addr(&self, addr: u64) -> Option<&Agent> {
self.agents.iter().find(|a| a.addr == addr)
}
pub fn agent_by_instance_id(&self, instance_id: u16) -> Option<&Agent> {
self.agents.iter().find(|a| a.instance_id == instance_id)
}
pub fn master_agent(&self, addr: u64) -> Option<&Agent> {
self.agent_by_addr(addr)
.and_then(|a| a.master_agent)
.and_then(|a| self.agent_by_addr(a))
}
pub fn players(&self) -> impl Iterator<Item = &Agent<Player>> {
self.agents.iter().filter_map(|a| a.as_player())
}
pub fn npcs(&self) -> impl Iterator<Item = &Agent<Character>> {
self.agents.iter().filter_map(|a| a.as_character())
}
pub fn boss(&self) -> &Agent {
self.npcs()
.find(|c| c.character().id == self.boss_id)
.map(Agent::erase)
.expect("Boss has no agent!")
}
pub fn boss_agents(&self) -> Vec<&Agent> {
let boss_ids = if self.boss_id == Boss::Xera as u16 {
vec![self.boss_id, gamedata::XERA_PHASE2_ID]
} else {
vec![self.boss_id]
};
self.npcs()
.filter(|c| boss_ids.contains(&c.character().id))
.map(Agent::erase)
.collect()
}
pub fn is_boss(&self, addr: u64) -> bool {
self.boss_agents().into_iter().any(|a| a.addr() == addr)
}
#[inline]
pub fn encounter_id(&self) -> u16 {
self.boss_id
}
#[inline]
pub fn encounter(&self) -> Option<Boss> {
Boss::from_u16(self.boss_id)
}
#[inline]
pub fn events(&self) -> &[Event] {
&self.events
}
}
impl Log {
pub fn is_cm(&self) -> bool {
let trigger = self
.encounter()
.map(Boss::cm_trigger)
.unwrap_or(CmTrigger::Unknown);
match trigger {
CmTrigger::HpThreshold(hp_threshold) => {
for event in self.events() {
if let EventKind::MaxHealthUpdate {
agent_addr,
max_health,
} = *event.kind()
{
if self.is_boss(agent_addr) && max_health >= hp_threshold as u64 {
return true;
}
}
}
false
}
CmTrigger::BuffPresent(wanted_buff_id) => {
for event in self.events() {
if let EventKind::BuffApplication { buff_id, .. } = *event.kind() {
if buff_id == wanted_buff_id {
return true;
}
}
}
false
}
CmTrigger::TimeBetweenBuffs(buff_id, threshold) => {
let tbb = time_between_buffs(&self.events, buff_id);
tbb != 0 && tbb <= threshold
}
CmTrigger::Always => true,
CmTrigger::None | CmTrigger::Unknown => false,
}
}
pub fn local_start_timestamp(&self) -> Option<u32> {
self.events().iter().find_map(|e| {
if let EventKind::LogStart {
local_timestamp, ..
} = e.kind()
{
Some(*local_timestamp)
} else {
None
}
})
}
pub fn local_end_timestamp(&self) -> Option<u32> {
self.events().iter().find_map(|e| {
if let EventKind::LogEnd {
local_timestamp, ..
} = e.kind()
{
Some(*local_timestamp)
} else {
None
}
})
}
pub fn was_rewarded(&self) -> bool {
self.events().iter().any(|e| {
if let EventKind::Reward { .. } = e.kind() {
true
} else {
false
}
})
}
}
fn time_between_buffs(events: &[Event], wanted_buff_id: u32) -> u64 {
let mut time_maps: HashMap<u64, Vec<u64>> = HashMap::new();
for event in events {
if let EventKind::BuffApplication {
destination_agent_addr,
buff_id,
..
} = event.kind()
{
if *buff_id == wanted_buff_id {
time_maps
.entry(*destination_agent_addr)
.or_default()
.push(event.time());
}
}
}
let timestamps = if let Some(ts) = time_maps.values().max_by_key(|v| v.len()) {
ts
} else {
return 0;
};
timestamps
.iter()
.zip(timestamps.iter().skip(1))
.map(|(a, b)| b - a)
.filter(|x| *x > 50)
.min()
.unwrap_or(0)
}