From d35534c0795caeda46e57fc515b74eba701110a2 Mon Sep 17 00:00:00 2001 From: Daniel Schadt Date: Fri, 6 Dec 2019 18:00:04 +0100 Subject: initial commit --- src/main.rs | 252 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 252 insertions(+) create mode 100644 src/main.rs (limited to 'src/main.rs') diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..9ffa1eb --- /dev/null +++ b/src/main.rs @@ -0,0 +1,252 @@ +extern crate clap; +extern crate image; +extern crate imageproc; +extern crate itertools; +extern crate md5; +extern crate reqwest; +extern crate rusttype; +extern crate xdg; +#[macro_use] +extern crate quick_error; + +use std::error::Error as StdError; +use std::fmt; + +mod api; +mod bt; +mod cache; +mod render; + +use clap::{App, Arg, ArgMatches}; + +use api::{Api, Profession, Skill}; +use bt::{BuildTemplate, TraitChoice, Traitline}; + +const APP_NAME: &str = "kondou"; + +#[derive(Debug, Clone)] +enum Error { + ProfessionNotFound(String), + SkillIdNotFound(u32), + SkillNotFound(String), + SpecializationNotFound(String), +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + Error::ProfessionNotFound(ref name) => { + write!(f, "the profession '{}' could not be found", name) + } + Error::SkillIdNotFound(id) => write!(f, "the skill with the ID '{}' was not found", id), + Error::SkillNotFound(ref name) => { + write!(f, "the skill with the name '{}' was not found", name) + } + Error::SpecializationNotFound(ref name) => write!( + f, + "the specialization with the name '{}' was not found", + name + ), + } + } +} + +impl StdError for Error {} + +type MainResult = Result>; + +fn find_profession(api: &mut Api, name: &str) -> MainResult { + let profession_ids = api.get_profession_ids()?; + let lower_name = name.to_lowercase(); + let profession_id = profession_ids + .iter() + .find(|id| id.to_lowercase() == lower_name) + .ok_or_else(|| Error::ProfessionNotFound(name.to_owned()))? + .clone(); + + Ok(api.get_professions(&[profession_id])?.remove(0)) +} + +fn resolve_skill(api: &mut Api, profession: &Profession, text: &str) -> MainResult { + // Try it as an ID first + let numeric = text.parse::(); + if let Ok(num_id) = numeric { + let exists = profession.skills.iter().any(|s| s.id == num_id); + if exists { + return Ok(api.get_skills(&[num_id])?.remove(0)); + } else { + return Err(Error::SkillIdNotFound(num_id).into()); + } + } + + // Check all names of the profession skills + let all_ids = profession.skills.iter().map(|s| s.id).collect::>(); + let all_skills = api.get_skills(&all_ids)?; + let lower_text = text.to_lowercase(); + all_skills + .into_iter() + .find(|s| s.name.to_lowercase().contains(&lower_text)) + .ok_or_else(|| Error::SkillNotFound(text.to_owned()).into()) +} + +fn resolve_traitline(api: &mut Api, profession: &Profession, text: &str) -> MainResult { + let parts = text.split(':').collect::>(); + assert_eq!( + parts.len(), + 4, + "invalid text format passed to resolve_traitline" + ); + + let name = parts[0]; + let lower_name = name.to_lowercase(); + let spec = api + .get_specializations(&profession.specializations)? + .into_iter() + .find(|s| s.name.to_lowercase() == lower_name) + .ok_or_else(|| Error::SpecializationNotFound(name.to_owned()))?; + + let mut choices = [TraitChoice::None; 3]; + for (i, text_choice) in parts.iter().skip(1).enumerate() { + let choice = text_choice + .parse() + .expect("Argument validation failed us, there is an invalid value here"); + choices[i] = choice; + } + Ok((spec, choices)) +} + +fn run_searching(api: &mut Api, matches: &ArgMatches) -> MainResult { + let requested_profession = matches + .value_of("profession") + .expect("clap handles missing argument"); + + let profession = find_profession(api, requested_profession)?; + + let skills = matches + .values_of("skill") + .map(Iterator::collect::>) + .unwrap_or_default() + .into_iter() + .map(|s| resolve_skill(api, &profession, s)) + .collect::, _>>()?; + + let traitlines = matches + .values_of("traitline") + .map(Iterator::collect::>) + .unwrap_or_default() + .into_iter() + .map(|t| resolve_traitline(api, &profession, t)) + .collect::, _>>()?; + + assert!(skills.len() <= bt::SKILL_COUNT, "got too many skills"); + assert!( + traitlines.len() <= bt::TRAITLINE_COUNT, + "got too many traitlines" + ); + + let build = BuildTemplate::new( + profession + .name + .parse() + .expect("Profession object has unparseable name"), + &skills, + &traitlines, + ) + .expect("BuildTemplate could not be constructed"); + + Ok(build) +} + +fn validate_traitline_format(input: String) -> Result<(), String> { + let parts = input.split(':').collect::>(); + if parts.len() != 4 { + return Err("traitline format is line:trait_1:trait_2:trait_3".to_owned()); + } + + for part in parts.iter().skip(1) { + let parsed = part.parse::(); + if parsed.is_err() { + return Err(format!( + "{} is not a valid trait. Use top, middle or bottom.", + part + )); + } + } + + Ok(()) +} + +fn run() -> MainResult<()> { + let matches = App::new(APP_NAME) + .version("0.1") + .author("Daniel ") + .about("Renders GW2 skills and traits.") + .arg( + Arg::with_name("profession") + .help("Selects which profession to use.") + .required_unless("chatlink"), + ) + .arg( + Arg::with_name("skill") + .help("Selects a skill based on either the name or the ID.") + .takes_value(true) + .number_of_values(1) + .long("skill") + .short("s") + .multiple(true) + .max_values(bt::SKILL_COUNT as u64), + ) + .arg( + Arg::with_name("traitline") + .help("Selects a traitline.") + .takes_value(true) + .number_of_values(1) + .long("traitline") + .short("t") + .multiple(true) + .validator(validate_traitline_format) + .max_values(bt::TRAITLINE_COUNT as u64), + ) + .arg( + Arg::with_name("chatlink") + .help("Specifies a chat link to parse.") + .short("c") + .long("chatlink") + .takes_value(true) + .conflicts_with_all(&["profession", "skill"]), + ) + .arg( + Arg::with_name("outfile") + .help("Specifies the output filename") + .short("o") + .long("outfile") + .default_value("buildtemplate.png") + .takes_value(true), + ) + .get_matches(); + + let mut api = Api::new(cache::FileCache::new()); + let build = match matches.is_present("chatlink") { + false => run_searching(&mut api, &matches)?, + true => unimplemented!(), + }; + + let mut renderer = render::Renderer::new(&mut api, Default::default()); + let img = renderer.render_buildtemplate(&build).unwrap(); + let filename = matches.value_of("outfile").unwrap(); + img.save(filename)?; + + Ok(()) +} + +fn main() { + let result = run(); + if let Err(e) = result { + eprintln!("[Error] {}", e); + let mut source = e.source(); + while let Some(s) = source { + eprintln!(" caused by {}", s); + source = s.source(); + } + } +} -- cgit v1.2.3