extern crate structopt; #[macro_use] extern crate quick_error; extern crate chrono; extern crate colored; extern crate evtclib; extern crate rayon; extern crate regex; extern crate walkdir; use std::fs::File; use std::io::{self, BufReader}; use std::path::PathBuf; use std::str::FromStr; use chrono::NaiveDateTime; use regex::Regex; use structopt::StructOpt; use walkdir::{DirEntry, WalkDir}; use rayon::prelude::*; use evtclib::{AgentKind, AgentName, EventKind, Log}; mod errors; use errors::RuntimeError; mod output; mod filters; macro_rules! unwrap { ($p:pat = $e:expr => { $r:expr} ) => { if let $p = $e { $r } else { panic!("Pattern match failed!"); } }; } macro_rules! debug { ($($arg:tt)*) => { if debug_enabled() { use std::io::Write; let stderr = ::std::io::stderr(); let mut lock = stderr.lock(); write!(lock, "[d] "); writeln!(lock, $($arg)*); } } } static mut DEBUG_ENABLED: bool = false; /// Return whether or not debug output should be enabled. #[inline] fn debug_enabled() -> bool { unsafe { DEBUG_ENABLED } } /// A program that allows you to search through all your evtc logs for specific /// people. #[derive(StructOpt, Debug)] #[structopt(name = "raidgrep")] pub struct Opt { /// Path to the folder with logs. #[structopt( short = "d", long = "dir", default_value = ".", parse(from_os_str) )] path: PathBuf, /// The fields which should be searched. #[structopt(short = "f", long = "fields", default_value = "all")] field: SearchField, /// Only display fights with the given outcome. #[structopt(short = "o", long = "outcome")] outcome: Option, /// Disable colored output. #[structopt(long = "no-color")] no_color: bool, /// Print more debugging information to stderr. #[structopt(long = "debug")] debug: bool, /// The regular expression to search for. #[structopt(name = "EXPR")] expression: Regex, } /// A flag indicating which fields should be searched. #[derive(Debug, Copy, Clone, PartialEq, Eq)] enum SearchField { /// Search all fields. All, /// Only search the account name. Account, /// Only search the character name. Character, } impl SearchField { /// True if the state says that the account name should be searched. #[inline] fn search_account(&self) -> bool { *self == SearchField::All || *self == SearchField::Account } /// True if the state says that the character name should be searched. #[inline] fn search_character(&self) -> bool { *self == SearchField::All || *self == SearchField::Character } } impl FromStr for SearchField { type Err = &'static str; fn from_str(s: &str) -> Result { match s { "all" => Ok(SearchField::All), "account" => Ok(SearchField::Account), "character" => Ok(SearchField::Character), _ => Err("Must be all, account or character"), } } } /// A log that matches the search criteria. #[derive(Debug, Clone)] pub struct LogResult { /// The path to the log file. log_file: PathBuf, /// The time of the recording. time: NaiveDateTime, /// The name of the boss. boss_name: String, /// A vector of all participating players. players: Vec, /// The outcome of the fight. outcome: FightOutcome, } /// A player. #[derive(Debug, Clone)] pub struct Player { /// Account name of the player. account_name: String, /// Character name of the player. character_name: String, /// Profession (or elite specialization) as english name. profession: String, /// Subsquad that the player was in. subgroup: u8, } /// Outcome of the fight. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum FightOutcome { Success, Wipe, } impl FromStr for FightOutcome { type Err = &'static str; fn from_str(s: &str) -> Result { match s { "success" => Ok(FightOutcome::Success), "wipe" | "fail" => Ok(FightOutcome::Wipe), _ => Err("Must be success or wipe"), } } } fn main() { let opt = Opt::from_args(); if opt.no_color { colored::control::set_override(false); } if opt.debug { // We haven't started any threads here yet, so this is fine. unsafe { DEBUG_ENABLED = true }; } let result = grep(&opt); match result { Ok(_) => {} Err(e) => { eprintln!("Error: {}", e); } } } /// Check if the given entry represents a log file, based on the file name. fn is_log_file(entry: &DirEntry) -> bool { entry .file_name() .to_str() .map(|n| n.ends_with(".evtc") || n.ends_with(".evtc.zip")) .unwrap_or(false) } /// Run the grep search with the given options. fn grep(opt: &Opt) -> Result<(), RuntimeError> { let walker = WalkDir::new(&opt.path); let entries = walker.into_iter().collect::>(); entries.into_par_iter().try_for_each(|e| { let entry = e?; if is_log_file(&entry) { if let Some(result) = search_log(&entry, opt)? { output::colored(io::stdout(), &result)?; } } Ok(()) }) } /// Search the given single log. /// /// If the log matches, returns `Ok(Some(..))`. /// If the log doesn't match, returns `Ok(None)`. /// If there was a fatal error, returns `Err(..)`. fn search_log(entry: &DirEntry, opt: &Opt) -> Result, RuntimeError> { let mut input = BufReader::new(File::open(entry.path())?); let raw = if entry .file_name() .to_str() .map(|n| n.ends_with(".zip")) .unwrap_or(false) { evtclib::raw::parse_zip(&mut input) } else { evtclib::raw::parse_file(&mut input) }; let parsed = raw.ok().and_then(|m| evtclib::process(&m).ok()); let log = if let Some(e) = parsed { e } else { debug!("log file cannot be parsed: {:?}", entry.path()); return Ok(None); }; let info = extract_info(entry, &log); let take_log = filters::filter_name(&log, opt) && filters::filter_outcome(&info, opt); if take_log { Ok(Some(info)) } else { Ok(None) } } /// Extract human-readable information from the given log file. fn extract_info(entry: &DirEntry, log: &Log) -> LogResult { let bosses = log.boss_agents(); let boss_name = if bosses.len() >= 1 { unwrap! { AgentName::Single(s) = bosses[0].name() => { s } } } else { debug!("log file no boss agents: {:?}", entry.path()); "" }.into(); let mut players = log .players() .map(|p| { unwrap! { AgentKind::Player { profession, elite } = p.kind() => { unwrap! { AgentName::Player { account_name, character_name, subgroup, } = p.name() => { Player { account_name: account_name.clone(), character_name: character_name.clone(), profession: get_profession_name(*profession, *elite).into(), subgroup: *subgroup, } }}}} }).collect::>(); players.sort_by_key(|p| p.subgroup); LogResult { log_file: entry.path().to_path_buf(), time: NaiveDateTime::from_timestamp(get_start_timestamp(log) as i64, 0), boss_name, players, outcome: get_fight_outcome(log), } } /// Get the timestamp of the log start time. fn get_start_timestamp(log: &Log) -> u32 { for event in log.events() { if let EventKind::LogStart { local_timestamp, .. } = event.kind { return local_timestamp; } } 0 } /// Get the outcome of the fight. fn get_fight_outcome(log: &Log) -> FightOutcome { for event in log.events() { if let EventKind::Reward { .. } = event.kind { return FightOutcome::Success; } } FightOutcome::Wipe } /// Get the (english) name for the given profession/elite specialization. fn get_profession_name(profession: u32, elite: u32) -> &'static str { match (profession, elite) { (1, 0) => "Guardian", (2, 0) => "Warrior", (3, 0) => "Engineer", (4, 0) => "Ranger", (5, 0) => "Thief", (6, 0) => "Elementalist", (7, 0) => "Mesmer", (8, 0) => "Necromancer", (9, 0) => "Revenant", (1, 27) => "Dragonhunter", (2, 18) => "Berserker", (3, 43) => "Scrapper", (4, 5) => "Druid", (5, 7) => "Daredevil", (6, 48) => "Tempest", (7, 40) => "Chronomancer", (8, 34) => "Reaper", (9, 52) => "Herald", (1, 62) => "Firebrand", (2, 61) => "Spellbreaker", (3, 57) => "Holosmith", (4, 55) => "Soulbeast", (5, 58) => "Deadeye", (6, 56) => "Weaver", (7, 59) => "Mirage", (8, 60) => "Scourge", (9, 63) => "Renegade", _ => { debug!("Unknown spec (prof: {}, elite: {})", profession, elite); "Unknown" }, } }