extern crate structopt; #[macro_use] extern crate quick_error; extern crate chrono; extern crate colored; extern crate evtclib; extern crate humantime; extern crate num_traits; 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::{Duration, NaiveDateTime}; use num_traits::cast::FromPrimitive; use regex::Regex; use structopt::StructOpt; use walkdir::{DirEntry, WalkDir}; use evtclib::{AgentKind, AgentName, EventKind, Log}; mod errors; use errors::RuntimeError; mod output; mod filters; mod csl; use csl::CommaSeparatedList; 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] ").expect("debug write failed"); writeln!(lock, $($arg)*).expect("debug write failed"); } } } 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 = "*")] field: CommaSeparatedList, /// Only display fights with the given outcome. #[structopt(short = "o", long = "outcome", default_value = "*")] outcome: CommaSeparatedList, /// Invert the regular expression (show fights that do not match) #[structopt(short = "v", long = "invert-match")] invert: bool, /// Disable colored output. #[structopt(long = "no-color")] no_color: bool, /// Only show logs that are younger than the given time. #[structopt( short = "a", long = "younger", parse(try_from_str = "parse_time_arg") )] after: Option, /// Only show logs that are older than the given time. #[structopt( short = "b", long = "older", parse(try_from_str = "parse_time_arg") )] before: Option, /// 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, Hash)] enum SearchField { /// Only search the account name. Account, /// Only search the character name. Character, } impl FromStr for SearchField { type Err = &'static str; fn from_str(s: &str) -> Result { match s { "account" => Ok(SearchField::Account), "character" => Ok(SearchField::Character), _ => Err("Must be 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, Hash)] pub enum FightOutcome { Success, Wipe, } impl FromStr for FightOutcome { type Err = &'static str; fn from_str(s: &str) -> Result { match s { "success" | "kill" => Ok(FightOutcome::Success), "wipe" | "fail" => Ok(FightOutcome::Wipe), _ => Err("Must be success or wipe"), } } } fn parse_time_arg(input: &str) -> Result { if let Ok(duration) = humantime::parse_duration(input) { let now = chrono::Local::now().naive_local(); let chrono_dur = Duration::from_std(duration).expect("Duration out of range!"); return Ok(now - chrono_dur); } if let Ok(time) = humantime::parse_rfc3339_weak(input) { let timestamp = time .duration_since(std::time::SystemTime::UNIX_EPOCH) .unwrap() .as_secs(); return Ok(NaiveDateTime::from_timestamp(timestamp as i64, 0)); } Err("unknown time format") } 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") || n.ends_with(".zevtc")) .unwrap_or(false) } /// Run the grep search with the given options. fn grep(opt: &Opt) -> Result<(), RuntimeError> { rayon::scope(|s| { let walker = WalkDir::new(&opt.path); for entry in walker { let entry = entry?; s.spawn(move |_| { if is_log_file(&entry) { if let Some(result) = search_log(&entry, opt).unwrap() { output::colored(io::stdout(), &result).unwrap(); } } }); } 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") || n.ends_with(".zevtc")) .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) == !opt.invert && filters::filter_outcome(&info, opt) && filters::filter_time(&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 boss_name = get_encounter_name(log) .unwrap_or_else(|| { debug!( "log file has unknown boss: {:?} (id: {:#x})", entry.path(), log.boss_id() ); "unknown" }).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(i64::from(get_start_timestamp(log)), 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 encounter fn get_encounter_name(log: &Log) -> Option<&'static str> { use evtclib::statistics::gamedata::Boss; let boss = Boss::from_u16(log.boss_id())?; Some(match boss { Boss::ValeGuardian => "Vale Guardian", Boss::Gorseval => "Gorseval", Boss::Sabetha => "Sabetha", Boss::Slothasor => "Slothasor", Boss::Matthias => "Matthias", Boss::KeepConstruct => "Keep Construct", Boss::Xera => "Xera", Boss::Cairn => "Cairn", Boss::MursaatOverseer => "Mursaat Overseer", Boss::Samarog => "Samarog", Boss::Deimos => "Deimos", Boss::SoullessHorror => "Desmina", Boss::Dhuum => "Dhuum", Boss::ConjuredAmalgamate => "Conjured Amalgamate", Boss::LargosTwins => "Largos Twins", Boss::Qadim => "Qadim", Boss::Skorvald => "Skorvald", Boss::Artsariiv => "Artsariiv", Boss::Arkk => "Arkk", Boss::MAMA => "MAMA", Boss::Siax => "Siax the Corrupted", Boss::Ensolyss => "Ensolyss of the Endless Torment", }) } /// 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" } } }