diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/bt.rs | 112 | ||||
-rw-r--r-- | src/main.rs | 30 |
2 files changed, 130 insertions, 12 deletions
@@ -1,11 +1,29 @@ -use super::api::{Skill, Specialization}; -use std::{fmt, str::FromStr}; +use super::api::{Api, ApiError, Skill, Specialization}; +use num_enum::{IntoPrimitive, TryFromPrimitive}; +use std::{convert::TryFrom, fmt, str::FromStr}; + +quick_error! { + #[derive(Debug)] + pub enum ChatlinkError { + ApiError(err: ApiError) { + cause(err) + from() + } + MalformedInput { + description("The input link is malformed") + from(base64::DecodeError) + from(num_enum::TryFromPrimitiveError<Profession>) + from(num_enum::TryFromPrimitiveError<TraitChoice>) + from(num_enum::TryFromPrimitiveError<Legend>) + } + } +} /// The profession of the template. /// /// Can be cast to an `u8` to get the right ID for building chat links. #[repr(u8)] -#[derive(Debug, PartialEq, Eq, Hash, Copy, Clone)] +#[derive(Debug, PartialEq, Eq, Hash, Copy, Clone, IntoPrimitive, TryFromPrimitive)] pub enum Profession { Guardian = 1, Warrior = 2, @@ -45,7 +63,7 @@ impl FromStr for Profession { /// Represents the selected trait. #[repr(u8)] -#[derive(Debug, PartialEq, Eq, Hash, Copy, Clone)] +#[derive(Debug, PartialEq, Eq, Hash, Copy, Clone, IntoPrimitive, TryFromPrimitive)] pub enum TraitChoice { None = 0, Top = 1, @@ -70,7 +88,7 @@ impl FromStr for TraitChoice { /// Represents a revenenant legend. #[repr(u8)] -#[derive(Debug, PartialEq, Eq, Hash, Copy, Clone)] +#[derive(Debug, PartialEq, Eq, Hash, Copy, Clone, IntoPrimitive, TryFromPrimitive)] pub enum Legend { None = 0, Dragon = 13, @@ -218,10 +236,14 @@ impl BuildTemplate { self.traitlines.iter().filter(|x| x.is_some()).count() as u32 } + /// Returns the extra data associated with this build template. pub fn extra_data(&self) -> &ExtraData { &self.extra_data } + /// Serializes this build template into a chat link. + /// + /// The returned link is ready to be copy-and-pasted into Guild Wars 2. pub fn chatlink(&self) -> String { let mut bytes = vec![0x0Du8]; let prof_byte = self.profession() as u8; @@ -272,6 +294,76 @@ impl BuildTemplate { format!("[&{}]", base64::encode(&bytes)) } + + /// Takes a chat link from the game and parses it into a BuildTemplate. + /// + /// This needs api acccess in order to fetch the relevant skill and trait data. + pub fn from_chatlink(api: &mut Api, input: &str) -> Result<BuildTemplate, ChatlinkError> { + if !input.starts_with("[&") || !input.ends_with("]") { + return Err(ChatlinkError::MalformedInput); + } + let inner = &input[2..input.len() - 1]; + let mut bytes = base64::decode(inner)?; + + // Magic number + if bytes.len() != 44 || bytes[0] != 0x0D { + return Err(ChatlinkError::MalformedInput); + } + bytes.remove(0); + + let profession = Profession::try_from(bytes.remove(0))?; + println!("Profession: {}", profession); + + let mut traitlines = BuildTemplate::empty(profession).traitlines; + for i in 0..TRAITLINE_COUNT { + let spec_id = bytes.remove(0); + let trait_choices = bytes.remove(0); + if spec_id == 0 { + continue; + } + + let spec = api.get_specializations(&[spec_id as u32])?.remove(0); + let c_0 = TraitChoice::try_from(trait_choices & 0x3)?; + let c_1 = TraitChoice::try_from((trait_choices >> 2) & 0x3)?; + let c_2 = TraitChoice::try_from((trait_choices >> 4) & 0x3)?; + traitlines[i] = Some((spec, [c_0, c_1, c_2])); + } + + let mut skills = BuildTemplate::empty(profession).skills; + for i in 0..SKILL_COUNT { + // Terrestrial + let byte_1 = bytes.remove(0); + let byte_2 = bytes.remove(0); + let palette_id = byte_1 as u32 | (byte_2 as u32) << 8; + if palette_id != 0 { + let skill_id = palette_id_to_skill_id(palette_id); + let skill = api.get_skills(&[skill_id])?.remove(0); + skills[i] = Some(skill); + } + + // Aquatic + bytes.remove(0); + bytes.remove(0); + } + + let extra_data = match profession { + Profession::Revenant => { + let mut legends = [Legend::None; LEGEND_COUNT]; + for i in 0..LEGEND_COUNT { + legends[i] = Legend::try_from(bytes.remove(0))?; + } + ExtraData::Legends(legends) + } + _ => ExtraData::None, + }; + + Ok(BuildTemplate { + profession, + traitlines, + skills, + extra_data, + }) + } } static JSON_PALETTE: &str = include_str!("skill_palette.json"); @@ -285,3 +377,13 @@ fn skill_id_to_palette_id(input: u32) -> u32 { } 0 } + +fn palette_id_to_skill_id(input: u32) -> u32 { + let lookup_table: Vec<(u32, u32)> = serde_json::from_str(JSON_PALETTE).unwrap(); + for (skill, palette) in &lookup_table { + if *palette == input { + return *skill; + } + } + 0 +} diff --git a/src/main.rs b/src/main.rs index e0d0e89..b241eb5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,6 +4,7 @@ extern crate image; extern crate imageproc; extern crate itertools; extern crate md5; +extern crate num_enum; extern crate reqwest; extern crate rusttype; extern crate termcolor; @@ -21,7 +22,7 @@ mod cache; mod render; use clap::{App, Arg, ArgMatches}; -use termcolor::{StandardStream, WriteColor, ColorSpec, Color, ColorChoice}; +use termcolor::{Color, ColorChoice, ColorSpec, StandardStream, WriteColor}; use api::{Api, Profession, Skill}; use bt::{BuildTemplate, ExtraData, Legend, TraitChoice, Traitline}; @@ -189,6 +190,11 @@ fn run_searching(api: &mut Api, matches: &ArgMatches) -> MainResult<BuildTemplat Ok(build) } +fn run_chatlink(api: &mut Api, matches: &ArgMatches) -> MainResult<BuildTemplate> { + 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::<Vec<_>>(); if parts.len() != 4 { @@ -278,7 +284,7 @@ fn run() -> MainResult<()> { let mut api = Api::new(cache::FileCache::new()); let build = match matches.is_present("chatlink") { false => run_searching(&mut api, &matches)?, - true => unimplemented!(), + true => run_chatlink(&mut api, &matches)?, }; println!("Chat code: {}", build.chatlink()); @@ -297,14 +303,24 @@ fn main() { 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); - write!(stderr, "[Error]"); - stderr.reset(); - writeln!(stderr, " {}", e); + 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 { - eprintln!(" caused by {}", s); + 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(); + } } } } |