diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/errors.rs | 18 | ||||
| -rw-r--r-- | src/main.rs | 269 | 
2 files changed, 287 insertions, 0 deletions
| diff --git a/src/errors.rs b/src/errors.rs new file mode 100644 index 0000000..7cb1886 --- /dev/null +++ b/src/errors.rs @@ -0,0 +1,18 @@ +use std; +use walkdir; + +quick_error! { +    #[derive(Debug)] +    pub enum RuntimeError { +        Walkdir(err: walkdir::Error) { +            from() +            cause(err) +            display("File enumeration error: {}", err) +        } +        Io(err: std::io::Error) { +            from() +            cause(err) +            display("I/O error: {}", err) +        } +    } +} 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", +    } +} | 
