extern crate base64; extern crate clap; extern crate image; extern crate imageproc; extern crate itertools; extern crate md5; extern crate num_enum; extern crate num_traits; extern crate reqwest; extern crate rusttype; extern crate termcolor; extern crate xdg; #[macro_use] extern crate quick_error; use std::error::Error as StdError; use std::fmt; use std::io::Write; mod api; mod bt; mod cache; mod render; use clap::{App, Arg, ArgMatches}; use termcolor::{Color, ColorChoice, ColorSpec, StandardStream, WriteColor}; use api::{Api, Profession, Skill}; use bt::{BuildTemplate, ExtraData, Legend, 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 prof_enum = profession .name .parse() .expect("Profession object has unparseable name"); let legends = matches .values_of("legend") .map(Iterator::collect::>) .unwrap_or_default() .into_iter() .map(str::parse) .map(Result::unwrap) .collect::>(); let extra_data = if prof_enum == bt::Profession::Revenant { let mut array_legends = [Legend::None; 4]; for (i, l) in legends.iter().enumerate() { array_legends[i] = *l; } ExtraData::Legends(array_legends) } else { ExtraData::None }; let skills = if prof_enum != bt::Profession::Revenant { matches .values_of("skill") .map(Iterator::collect::>) .unwrap_or_default() .into_iter() .map(|s| resolve_skill(api, &profession, s)) .collect::, _>>()? } else if let Some(l) = legends.first() { let l = api.get_legends(&[l.get_api_id().unwrap()])?.remove(0); let mut result = Vec::new(); for skill_id in (&[l.heal]).iter().chain(&l.utilities).chain(&[l.elite]) { let skill = api.get_skills(&[*skill_id])?.remove(0); result.push(skill); } result } else { Vec::new() }; 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(prof_enum, &skills, &traitlines, extra_data) .expect("BuildTemplate could not be constructed"); Ok(build) } fn run_chatlink(api: &mut Api, matches: &ArgMatches) -> MainResult { let link = matches.value_of("chatlink").unwrap(); Ok(BuildTemplate::from_chatlink(api, link)?) } 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 validate_legend(input: String) -> Result<(), String> { input .parse::() .map(|_| ()) .map_err(|_| "invalid legend name".to_owned()) } 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("legend") .help("Selects a revenant legend.") .takes_value(true) .number_of_values(1) .long("legend") .short("l") .multiple(true) .max_values(bt::LEGEND_COUNT as u64) .validator(validate_legend), ) .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 = if matches.is_present("chatlink") { run_chatlink(&mut api, &matches)? } else { run_searching(&mut api, &matches)? }; println!("Chat code: {}", build.chatlink()); 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 { let mut error_color = ColorSpec::new(); error_color.set_fg(Some(Color::Red)); let mut stderr = StandardStream::stderr(ColorChoice::Auto); stderr.set_color(&error_color).unwrap(); write!(stderr, "[Error]").unwrap(); stderr.reset().unwrap(); writeln!(stderr, " {}", e).unwrap(); let mut source = e.source(); if source.is_none() { source = e.cause(); } while let Some(s) = source { stderr.set_color(&error_color).unwrap(); write!(stderr, " [caused by]").unwrap(); stderr.reset().unwrap(); writeln!(stderr, " {}", s).unwrap(); source = s.source(); if source.is_none() { source = s.cause(); } } } }