aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/errors.rs18
-rw-r--r--src/main.rs269
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",
+ }
+}