aboutsummaryrefslogtreecommitdiff
path: root/src/main.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/main.rs')
-rw-r--r--src/main.rs252
1 files changed, 252 insertions, 0 deletions
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<T> = Result<T, Box<dyn StdError>>;
+
+fn find_profession(api: &mut Api, name: &str) -> MainResult<Profession> {
+ 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<Skill> {
+ // Try it as an ID first
+ let numeric = text.parse::<u32>();
+ 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::<Vec<_>>();
+ 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<Traitline> {
+ let parts = text.split(':').collect::<Vec<_>>();
+ 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<BuildTemplate> {
+ 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::<Vec<_>>)
+ .unwrap_or_default()
+ .into_iter()
+ .map(|s| resolve_skill(api, &profession, s))
+ .collect::<Result<Vec<_>, _>>()?;
+
+ let traitlines = matches
+ .values_of("traitline")
+ .map(Iterator::collect::<Vec<_>>)
+ .unwrap_or_default()
+ .into_iter()
+ .map(|t| resolve_traitline(api, &profession, t))
+ .collect::<Result<Vec<_>, _>>()?;
+
+ 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::<Vec<_>>();
+ 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::<TraitChoice>();
+ 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 <kingdread@gmx.de>")
+ .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();
+ }
+ }
+}