aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/csl.rs2
-rw-r--r--src/filters.rs23
-rw-r--r--src/guilds.rs79
-rw-r--r--src/main.rs81
-rw-r--r--src/output/formats.rs23
-rw-r--r--src/output/mod.rs5
6 files changed, 181 insertions, 32 deletions
diff --git a/src/csl.rs b/src/csl.rs
index 6609dda..e7d84f3 100644
--- a/src/csl.rs
+++ b/src/csl.rs
@@ -36,7 +36,7 @@ macro_rules! variants {
};
}
-variants! { SearchField => Account, Character }
+variants! { SearchField => Account, Character, Guild }
variants! { FightOutcome => Success, Wipe }
variants! { Weekday => Mon, Tue, Wed, Thu, Fri, Sat, Sun }
variants! { Boss =>
diff --git a/src/filters.rs b/src/filters.rs
index 02334bc..715e4e4 100644
--- a/src/filters.rs
+++ b/src/filters.rs
@@ -4,7 +4,7 @@ use evtclib::statistics::gamedata::Boss;
use num_traits::FromPrimitive;
-use super::{SearchField, LogResult, Opt};
+use super::{SearchField, LogResult, Opt, guilds};
use chrono::Datelike;
@@ -30,7 +30,8 @@ pub fn filter_name(evtc: &PartialEvtc, opt: &Opt) -> bool {
_ => (),
}
}
- false
+ // Don't throw away the log yet if we are searching for guilds
+ opt.field.contains(&SearchField::Guild)
}
/// Do filtering based on the boss ID.
@@ -64,3 +65,21 @@ pub fn filter_time(result: &LogResult, opt: &Opt) -> bool {
after_ok && before_ok
}
+
+/// Do filtering based on the guilds.
+pub fn filter_guilds(result: &LogResult, opt: &Opt) -> bool {
+ if !opt.guilds {
+ return true;
+ }
+ if !opt.field.contains(&SearchField::Guild) {
+ return true;
+ }
+ result.players.iter().any(|player| {
+ let guild = player.guild_id.as_ref().and_then(|id| guilds::lookup(id));
+ if let Some(guild) = guild {
+ opt.expression.is_match(guild.tag()) || opt.expression.is_match(guild.name())
+ } else {
+ false
+ }
+ })
+}
diff --git a/src/guilds.rs b/src/guilds.rs
new file mode 100644
index 0000000..0eff6ad
--- /dev/null
+++ b/src/guilds.rs
@@ -0,0 +1,79 @@
+//! Guild name retrieval and caching functions.
+use std::collections::HashMap;
+use std::fs::File;
+use std::path::PathBuf;
+use std::sync::RwLock;
+
+use once_cell::sync::Lazy;
+use serde::{Deserialize, Serialize};
+
+static CACHE: Lazy<RwLock<HashMap<String, Guild>>> = Lazy::new(|| RwLock::new(HashMap::new()));
+
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+pub struct Guild {
+ tag: String,
+ name: String,
+}
+
+impl Guild {
+ pub fn tag(&self) -> &str {
+ &self.tag
+ }
+
+ pub fn name(&self) -> &str {
+ &self.name
+ }
+}
+
+/// Looks up the given guild.
+///
+/// This checks the cache first, and if nothing was found, it will hit the API.
+pub fn lookup(api_id: &str) -> Option<Guild> {
+ {
+ let cache = CACHE.read().unwrap();
+ if let Some(guild) = cache.get(api_id) {
+ return Some(guild.clone());
+ }
+ }
+
+ let mut cache = CACHE.write().unwrap();
+ let url = format!("https://api.guildwars2.com/v2/guild/{}", api_id);
+ let result = ureq::get(&url)
+ .call()
+ .into_json()
+ .expect("Invalid JSON in API response");
+ let name = result["name"].as_str()?;
+ let tag = result["tag"].as_str()?;
+ let guild = Guild {
+ tag: tag.into(),
+ name: name.into(),
+ };
+ cache.insert(api_id.into(), guild.clone());
+ Some(guild)
+}
+
+fn cache_path() -> PathBuf {
+ let mut cache_path = dirs::cache_dir().unwrap();
+ cache_path.push("raidgrep");
+ cache_path
+}
+
+/// Loads the cache from the file system.
+pub fn prepare_cache() {
+ let path = cache_path();
+ if !path.is_file() {
+ return;
+ }
+
+ let file = File::open(path).expect("Unable to read cache file");
+ let cache = serde_json::from_reader(file).expect("Cache file has invalid format");
+ let mut target = CACHE.write().unwrap();
+ *target = cache;
+}
+
+/// Saves the cache to the file system
+pub fn save_cache() {
+ let path = cache_path();
+ let file = File::create(path).expect("Cannot open cache for writing");
+ serde_json::to_writer(file, &*CACHE.read().unwrap()).unwrap();
+}
diff --git a/src/main.rs b/src/main.rs
index 838a518..f82e522 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,19 +1,20 @@
+use std::collections::HashMap;
use std::fs::File;
use std::io::{BufReader, Read, Seek};
use std::path::PathBuf;
use std::str::FromStr;
+use anyhow::{anyhow, Result};
use chrono::{Duration, NaiveDateTime, Weekday};
use num_traits::cast::FromPrimitive;
use regex::Regex;
use structopt::StructOpt;
use walkdir::{DirEntry, WalkDir};
-use anyhow::{anyhow, Result};
use evtclib::{AgentKind, AgentName, EventKind, Log};
+mod guilds;
mod output;
-
mod filters;
mod csl;
@@ -55,16 +56,11 @@ fn debug_enabled() -> bool {
#[structopt(name = "raidgrep")]
pub struct Opt {
/// Path to the folder with logs.
- #[structopt(
- short = "d",
- long = "dir",
- default_value = ".",
- parse(from_os_str)
- )]
+ #[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 = "*")]
+ #[structopt(short = "f", long = "fields", default_value = "account,character")]
field: CommaSeparatedList<SearchField>,
/// Only display fights with the given outcome.
@@ -109,17 +105,17 @@ pub struct Opt {
weekdays: CommaSeparatedList<Weekday>,
/// Only show logs from the given encounters.
- #[structopt(
- short = "e",
- long = "bosses",
- default_value = "*",
- )]
+ #[structopt(short = "e", long = "bosses", default_value = "*")]
bosses: CommaSeparatedList<evtclib::statistics::gamedata::Boss>,
/// Print more debugging information to stderr.
#[structopt(long = "debug")]
debug: bool,
+ /// Load guild information from the API.
+ #[structopt(long = "guilds")]
+ guilds: bool,
+
/// The regular expression to search for.
#[structopt(name = "EXPR")]
expression: Regex,
@@ -132,6 +128,8 @@ enum SearchField {
Account,
/// Only search the character name.
Character,
+ /// Only search the guild name or tag.
+ Guild,
}
impl FromStr for SearchField {
@@ -141,6 +139,7 @@ impl FromStr for SearchField {
match s {
"account" => Ok(SearchField::Account),
"character" => Ok(SearchField::Character),
+ "guild" => Ok(SearchField::Guild),
_ => Err("Must be account or character"),
}
}
@@ -172,6 +171,8 @@ pub struct Player {
profession: String,
/// Subsquad that the player was in.
subgroup: u8,
+ /// Guild ID, ready for API consumption.
+ guild_id: Option<String>,
}
/// Outcome of the fight.
@@ -209,12 +210,10 @@ fn parse_time_arg(input: &str) -> Result<NaiveDateTime> {
Err(anyhow!("unknown time format"))
}
-fn try_from_str_simple_error<T: FromStr>(input: &str) -> Result<T, String>
-{
+fn try_from_str_simple_error<T: FromStr>(input: &str) -> Result<T, String> {
T::from_str(input).map_err(|_| format!("'{}' is an invalid value", input))
}
-
enum ZipWrapper<R: Read + Seek> {
Raw(Option<R>),
Zipped(zip::ZipArchive<R>),
@@ -237,7 +236,6 @@ impl<R: Read + Seek> ZipWrapper<R> {
}
}
-
fn main() {
let opt = Opt::from_args();
@@ -250,6 +248,10 @@ fn main() {
unsafe { DEBUG_ENABLED = true };
}
+ if opt.guilds {
+ guilds::prepare_cache();
+ }
+
let result = grep(&opt);
match result {
Ok(_) => {}
@@ -257,6 +259,10 @@ fn main() {
eprintln!("Error: {}", e);
}
}
+
+ if opt.guilds {
+ guilds::save_cache();
+ }
}
/// Check if the given entry represents a log file, based on the file name.
@@ -311,11 +317,11 @@ fn search_log(entry: &DirEntry, opt: &Opt) -> Result<Option<LogResult>> {
let mut stream = wrapper.get_stream();
let partial = evtclib::raw::parser::parse_partial_file(&mut stream)?;
- let early_ok = filters::filter_name(&partial, opt) != opt.invert
- && filters::filter_boss(&partial, opt);
+ let early_ok =
+ filters::filter_name(&partial, opt) != opt.invert && filters::filter_boss(&partial, opt);
if !early_ok {
- return Ok(None)
+ return Ok(None);
}
let raw = evtclib::raw::parser::finish_parsing(partial, &mut stream)?;
@@ -331,7 +337,8 @@ fn search_log(entry: &DirEntry, opt: &Opt) -> Result<Option<LogResult>> {
let take_log = filters::filter_outcome(&info, opt)
&& filters::filter_weekday(&info, opt)
- && filters::filter_time(&info, opt);
+ && filters::filter_time(&info, opt)
+ && filters::filter_guilds(&info, opt);
if take_log {
Ok(Some(info))
@@ -350,7 +357,10 @@ fn extract_info(entry: &DirEntry, log: &Log) -> LogResult {
log.boss_id()
);
"unknown"
- }).into();
+ })
+ .into();
+
+ let guild_ids = get_guild_mapping(log);
let mut players = log
.players()
@@ -367,9 +377,11 @@ fn extract_info(entry: &DirEntry, log: &Log) -> LogResult {
character_name: character_name.clone(),
profession: get_profession_name(*profession, *elite).into(),
subgroup: *subgroup,
+ guild_id: guild_ids.get(p.addr()).cloned(),
}
}}}}
- }).collect::<Vec<Player>>();
+ })
+ .collect::<Vec<Player>>();
players.sort_by_key(|p| p.subgroup);
LogResult {
@@ -381,6 +393,27 @@ fn extract_info(entry: &DirEntry, log: &Log) -> LogResult {
}
}
+/// Get a mapping of agent IDs to guild API strings.
+fn get_guild_mapping(log: &Log) -> HashMap<u64, String> {
+ log.events()
+ .iter()
+ .filter_map(|event| {
+ if let EventKind::Guild {
+ source_agent_addr,
+ ref api_guild_id,
+ ..
+ } = event.kind
+ {
+ api_guild_id
+ .as_ref()
+ .map(|api_id| (source_agent_addr, api_id.clone()))
+ } else {
+ None
+ }
+ })
+ .collect()
+}
+
/// Get the timestamp of the log start time.
fn get_start_timestamp(log: &Log) -> u32 {
for event in log.events() {
diff --git a/src/output/formats.rs b/src/output/formats.rs
index 5915069..b697401 100644
--- a/src/output/formats.rs
+++ b/src/output/formats.rs
@@ -2,6 +2,7 @@
use std::fmt::Write;
use super::{LogResult, FightOutcome};
+use super::super::guilds;
/// An output format
pub trait Format: Sync + Send {
@@ -12,7 +13,9 @@ pub trait Format: Sync + Send {
/// The human readable, colored format.
#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)]
-pub struct HumanReadable;
+pub struct HumanReadable {
+ pub show_guilds: bool,
+}
impl Format for HumanReadable {
@@ -35,14 +38,26 @@ impl Format for HumanReadable {
outcome,
).unwrap();
for player in &item.players {
- writeln!(
+ write!(
result,
- " {:2} {:20} {:19} {}",
+ " {:2} {:20} {:19} {:12}",
player.subgroup,
player.account_name.yellow(),
player.character_name.cyan(),
- player.profession
+ player.profession,
).unwrap();
+ if self.show_guilds {
+ let guild = player.guild_id.as_ref().and_then(|id| guilds::lookup(id));
+ if let Some(guild) = guild {
+ write!(
+ result,
+ " [{}] {}",
+ guild.tag().magenta(),
+ guild.name().magenta(),
+ ).unwrap();
+ }
+ }
+ writeln!(result).unwrap();
}
result
}
diff --git a/src/output/mod.rs b/src/output/mod.rs
index 73af5ab..84ed0a4 100644
--- a/src/output/mod.rs
+++ b/src/output/mod.rs
@@ -17,6 +17,9 @@ pub fn build_pipeline(opt: &Opt) -> Pipeline {
if opt.file_name_only {
Pipeline::new(stream, formats::FileOnly, aggregator)
} else {
- Pipeline::new(stream, formats::HumanReadable, aggregator)
+ let format = formats::HumanReadable {
+ show_guilds: opt.guilds,
+ };
+ Pipeline::new(stream, format, aggregator)
}
}