use super::api::{Api, ApiError, Profession, Skill, Specialization}; use byteorder::{ReadBytesExt, WriteBytesExt, LE}; use num_enum::{IntoPrimitive, TryFromPrimitive}; use std::{convert::TryFrom, fmt, io::Cursor, str::FromStr}; use thiserror::Error; #[derive(Error, Debug)] pub enum ChatlinkError { #[error("Error accessing the API")] ApiError(#[from] ApiError), #[error("The input link is malformed")] MalformedInput, } impl From for ChatlinkError { fn from(_err: std::io::Error) -> Self { panic!("The reading cursor should never return an error!"); } } impl From for ChatlinkError { fn from(_: base64::DecodeError) -> Self { ChatlinkError::MalformedInput } } impl From> for ChatlinkError { fn from(_: num_enum::TryFromPrimitiveError) -> Self { ChatlinkError::MalformedInput } } impl From> for ChatlinkError { fn from(_: num_enum::TryFromPrimitiveError) -> Self { ChatlinkError::MalformedInput } } /// Represents the selected trait. #[repr(u8)] #[derive(Debug, PartialEq, Eq, Hash, Copy, Clone, IntoPrimitive, TryFromPrimitive)] pub enum TraitChoice { None = 0, Top = 1, Middle = 2, Bottom = 3, } impl FromStr for TraitChoice { type Err = (); fn from_str(s: &str) -> Result { let lower = s.to_lowercase(); match &lower as &str { "" | "none" => Ok(TraitChoice::None), "top" => Ok(TraitChoice::Top), "mid" | "middle" => Ok(TraitChoice::Middle), "bot" | "bottom" => Ok(TraitChoice::Bottom), _ => Err(()), } } } impl fmt::Display for TraitChoice { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let result = match *self { TraitChoice::None => "none", TraitChoice::Top => "top", TraitChoice::Middle => "mid", TraitChoice::Bottom => "bot", }; write!(f, "{}", result) } } /// Represents a revenenant legend. #[repr(u8)] #[derive(Debug, PartialEq, Eq, Hash, Copy, Clone, IntoPrimitive, TryFromPrimitive)] pub enum Legend { None = 0, Dragon = 13, Assassin = 14, Dwarf = 15, Demon = 16, Renegade = 17, Centaur = 18, } impl Legend { pub fn api_id(self) -> Option { Some( match self { Legend::None => return None, Legend::Dragon => "Legend1", Legend::Assassin => "Legend2", Legend::Dwarf => "Legend3", Legend::Demon => "Legend4", Legend::Renegade => "Legend5", Legend::Centaur => "Legend6", } .to_owned(), ) } } impl FromStr for Legend { type Err = (); fn from_str(s: &str) -> Result { let lower = s.to_lowercase(); match &lower as &str { "" | "none" => Ok(Legend::None), "dragon" | "glint" => Ok(Legend::Dragon), "assassin" | "shiro" => Ok(Legend::Assassin), "dwarf" | "jalis" => Ok(Legend::Dwarf), "demon" | "mallyx" => Ok(Legend::Demon), "renegade" | "kalla" => Ok(Legend::Renegade), "centaur" | "ventari" => Ok(Legend::Centaur), _ => Err(()), } } } #[derive(Debug, Clone)] pub enum ExtraData { None, Legends([Legend; 4]), } /// Represents a traitline. /// /// A traitline consists of the chosen specialization including 3 possible trait choices. pub type Traitline = (Specialization, [TraitChoice; 3]); pub const SKILL_COUNT: usize = 5; pub const TRAITLINE_COUNT: usize = 3; pub const LEGEND_COUNT: usize = 4; /// The code for the revenant profession. pub const CODE_REVENANT: u32 = 9; pub const EMPTY_SKILLS: [Option; SKILL_COUNT] = [None, None, None, None, None]; pub const EMPTY_TRAITLINES: [Option; TRAITLINE_COUNT] = [None, None, None]; /// Represents a build template. /// /// This struct is made with the same limitations as the game imposes. That is, even though the /// renderer can support more than three traitlines, this template will only allow you to take /// three. #[derive(Debug)] pub struct BuildTemplate { /// Profession of this build. profession: Profession, /// The skills of the build. /// /// Each slot can either contain a slot or be empty. A maximum of 5 skills are allowed (heal, 3 /// utilities and elite). skills: [Option; SKILL_COUNT], /// The traitlines of the build. traitlines: [Option; TRAITLINE_COUNT], /// Extra data, such as revenant legends or ranger pets. extra_data: ExtraData, } impl BuildTemplate { /// Creates a template with the given skills and traitlines. /// /// If there are more than 5 skills or 3 traitlines given, the function will return `None`. pub fn new( profession: Profession, skills: &[Skill], traitlines: &[Traitline], extra_data: ExtraData, ) -> Option { if skills.len() > SKILL_COUNT { return None; } if traitlines.len() > TRAITLINE_COUNT { return None; } let mut skill_array = [None, None, None, None, None]; for (i, skill) in skills.iter().enumerate() { skill_array[i] = Some(skill.clone()); } let mut trait_array = [None, None, None]; for (i, traitline) in traitlines.iter().enumerate() { trait_array[i] = Some(traitline.clone()); } Some(BuildTemplate { profession, skills: skill_array, traitlines: trait_array, extra_data, }) } /// Returns the profession of this build. pub fn profession(&self) -> &Profession { &self.profession } /// Returns the skills of this build. pub fn skills(&self) -> &[Option] { &self.skills } /// Returns the number of actually equipped skills. pub fn skill_count(&self) -> u32 { self.skills.iter().filter(|x| x.is_some()).count() as u32 } /// Returns the traitlines of this build. pub fn traitlines(&self) -> &[Option] { &self.traitlines } /// Returns the number of actually equipped specializations. pub fn traitline_count(&self) -> u32 { 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().code as u8; bytes.push(prof_byte); for traitline in self.traitlines().iter() { if let Some((spec, choices)) = traitline { bytes.push(spec.id as u8); let selected = choices[0] as u8 | (choices[1] as u8) << 2 | (choices[2] as u8) << 4; bytes.push(selected); } else { bytes.push(0); bytes.push(0); } } for skill in self.skills() { // Terrestric match skill { None => { bytes.push(0); bytes.push(0); } Some(s) => { let palette_id = self.profession().skill_id_to_palette_id(s.id).unwrap_or(0); bytes.write_u16::(palette_id as u16).unwrap(); } } // Aquatic bytes.push(0); bytes.push(0); } match *self.extra_data() { ExtraData::None => { bytes.extend_from_slice(&[0, 0, 0, 0]); } ExtraData::Legends(ref legends) => { bytes.extend(legends.iter().map(|l| *l as u8)); } } // 12 more bytes are used to save the order of the skills in the inactive revenant // utilities (the active ones are saved in the normal skill slots). // The order is terrestric 1/2/3 and then aquatic 1/2/3, with 2 bytes per skill. // We don't care about that, so just do whatever. for _ in 0..12 { bytes.push(0); } 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 { 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 mut reader = Cursor::new(bytes); let profession = code_to_profession(api, reader.read_u8()? as u32)?; let mut traitlines = EMPTY_TRAITLINES; for i in traitlines.iter_mut() { let spec_id = reader.read_u8()?; let trait_choices = reader.read_u8()?; 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)?; *i = Some((spec, [c_0, c_1, c_2])); } let mut skills = EMPTY_SKILLS; for i in skills.iter_mut() { // Terrestrial let palette_id = reader.read_u16::()? as u32; if palette_id != 0 { let skill_id = profession .palette_id_to_skill_id(palette_id) .ok_or(ChatlinkError::MalformedInput)?; let skill = api.get_skills(&[skill_id])?.remove(0); *i = Some(skill); } // Aquatic reader.read_u16::()?; } let extra_data = match profession.code { CODE_REVENANT => { let mut legends = [Legend::None; LEGEND_COUNT]; for i in legends.iter_mut() { *i = Legend::try_from(reader.read_u8()?)?; } ExtraData::Legends(legends) } _ => ExtraData::None, }; Ok(BuildTemplate { profession, traitlines, skills, extra_data, }) } } fn code_to_profession(api: &mut Api, code: u32) -> Result { let professions = api.get_all_professions()?; professions .into_iter() .find(|p| p.code == code) .ok_or(ChatlinkError::MalformedInput) }