diff options
Diffstat (limited to 'src/main.rs')
-rw-r--r-- | src/main.rs | 269 |
1 files changed, 269 insertions, 0 deletions
diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..502d98e --- /dev/null +++ b/src/main.rs @@ -0,0 +1,269 @@ +extern crate structopt; +#[macro_use] +extern crate quick_error; +extern crate chrono; +extern crate colored; +extern crate evtclib; +extern crate regex; +extern crate walkdir; + +use std::fmt; +use std::fs::File; +use std::io::BufReader; +use std::path::PathBuf; +use std::str::FromStr; + +use chrono::NaiveDateTime; +use regex::Regex; +use structopt::StructOpt; +use walkdir::{DirEntry, WalkDir}; + +use evtclib::{AgentKind, AgentName, EventKind, Log}; + +mod errors; +use errors::RuntimeError; + +#[derive(StructOpt, Debug)] +#[structopt(name = "raidgrep")] +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. + /// Possible values: all, account, character + #[structopt(short = "f", long = "fields", default_value = "all")] + field: SearchField, + + /// The expression to search for. + #[structopt(name = "EXPR")] + expression: Regex, +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +enum SearchField { + All, + Account, + Character, +} + +impl FromStr for SearchField { + type Err = &'static str; + + fn from_str(s: &str) -> Result<Self, Self::Err> { + match s { + "all" => Ok(SearchField::All), + "account" => Ok(SearchField::Account), + "character" => Ok(SearchField::Character), + _ => Err("Must be all, account or character"), + } + } +} + +#[derive(Debug, Clone)] +struct LogResult { + log_file: PathBuf, + time: NaiveDateTime, + boss_name: String, + players: Vec<Player>, +} + +#[derive(Debug, Clone)] +struct Player { + account_name: String, + character_name: String, + profession: String, + subgroup: u8, +} + +impl fmt::Display for LogResult { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + use colored::Colorize; + writeln!(f, "{}: {:?}", "File".green(), self.log_file)?; + writeln!( + f, + "{}: {} - {}: {}", + "Date".green(), + self.time, + "Boss".green(), + self.boss_name + )?; + for player in &self.players { + writeln!( + f, + " {:2} {:20} {:19} {}", + player.subgroup, + player.account_name.yellow(), + player.character_name.cyan(), + player.profession + )?; + } + Ok(()) + } +} + +fn main() { + let opt = Opt::from_args(); + let result = grep(&opt); + match result { + Ok(_) => {} + Err(e) => { + eprintln!("Error: {}", e); + } + } +} + +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) +} + +fn grep(opt: &Opt) -> Result<(), RuntimeError> { + let walker = WalkDir::new(&opt.path); + for entry in walker { + let entry = entry?; + if is_log_file(&entry) { + if let Some(result) = search_log(&entry, opt)? { + println!("{}", result); + } + } + } + + Ok(()) +} + +fn search_log(entry: &DirEntry, opt: &Opt) -> Result<Option<LogResult>, 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 { + return Ok(None); + }; + + for player in log.players() { + match player.name() { + AgentName::Player { + account_name, + character_name, + .. + } => { + if ((opt.field == SearchField::All || opt.field == SearchField::Account) + && opt.expression.is_match(account_name)) + || ((opt.field == SearchField::All || opt.field == SearchField::Character) + && opt.expression.is_match(character_name)) + { + return Ok(Some(extract_info(entry, &log))); + } + } + _ => unreachable!(), + } + } + + Ok(None) +} + +fn extract_info(entry: &DirEntry, log: &Log) -> LogResult { + let boss_name = match log.boss().name() { + AgentName::Single(s) => s, + _ => "<unknown>", + }.into(); + + let mut players = log + .players() + .map(|p| { + if let AgentKind::Player { profession, elite } = p.kind() { + if let 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, + } + } else { + unreachable!() + } + } else { + unreachable!() + } + }).collect::<Vec<Player>>(); + 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, + } +} + +fn get_start_timestamp(log: &Log) -> u32 { + for event in log.events() { + if let EventKind::LogStart { + local_timestamp, .. + } = event.kind + { + return local_timestamp; + } + } + 0 +} + +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", + + _ => "Unknown", + } +} |