diff options
Diffstat (limited to 'src/bt.rs')
-rw-r--r-- | src/bt.rs | 140 |
1 files changed, 47 insertions, 93 deletions
@@ -1,6 +1,7 @@ -use super::api::{Api, ApiError, Skill, Specialization}; +use super::api::{Api, ApiError, Profession, Skill, Specialization}; +use byteorder::{ReadBytesExt, WriteBytesExt, LE}; use num_enum::{IntoPrimitive, TryFromPrimitive}; -use std::{convert::TryFrom, error::Error, fmt, str::FromStr}; +use std::{convert::TryFrom, error::Error, fmt, io::Cursor, str::FromStr}; #[derive(Debug)] pub enum ChatlinkError { @@ -11,11 +12,16 @@ pub enum ChatlinkError { error_froms! { ChatlinkError, err: ApiError => ChatlinkError::ApiError(err), _err: base64::DecodeError => ChatlinkError::MalformedInput, - _err: num_enum::TryFromPrimitiveError<Profession> => ChatlinkError::MalformedInput, _err: num_enum::TryFromPrimitiveError<TraitChoice> => ChatlinkError::MalformedInput, _err: num_enum::TryFromPrimitiveError<Legend> => ChatlinkError::MalformedInput, } +impl From<std::io::Error> for ChatlinkError { + fn from(_err: std::io::Error) -> Self { + panic!("The reading cursor should never return an error!"); + } +} + impl fmt::Display for ChatlinkError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match *self { @@ -34,48 +40,6 @@ impl Error for ChatlinkError { } } -/// 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, IntoPrimitive, TryFromPrimitive)] -pub enum Profession { - Guardian = 1, - Warrior = 2, - Engineer = 3, - Ranger = 4, - Thief = 5, - Elementalist = 6, - Mesmer = 7, - Necromancer = 8, - Revenant = 9, -} - -impl fmt::Display for Profession { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - fmt::Debug::fmt(&self, f) - } -} - -impl FromStr for Profession { - type Err = (); - - fn from_str(s: &str) -> Result<Self, Self::Err> { - match s { - "Guardian" => Ok(Profession::Guardian), - "Warrior" => Ok(Profession::Warrior), - "Engineer" => Ok(Profession::Engineer), - "Ranger" => Ok(Profession::Ranger), - "Thief" => Ok(Profession::Thief), - "Elementalist" => Ok(Profession::Elementalist), - "Mesmer" => Ok(Profession::Mesmer), - "Necromancer" => Ok(Profession::Necromancer), - "Revenant" => Ok(Profession::Revenant), - _ => Err(()), - } - } -} - /// Represents the selected trait. #[repr(u8)] #[derive(Debug, PartialEq, Eq, Hash, Copy, Clone, IntoPrimitive, TryFromPrimitive)] @@ -176,6 +140,12 @@ 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>; SKILL_COUNT] = [None, None, None, None, None]; +pub const EMPTY_TRAITLINES: [Option<Traitline>; 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 @@ -201,8 +171,8 @@ impl BuildTemplate { pub fn empty(profession: Profession) -> BuildTemplate { BuildTemplate { profession, - skills: [None, None, None, None, None], - traitlines: [None, None, None], + skills: EMPTY_SKILLS, + traitlines: EMPTY_TRAITLINES, extra_data: ExtraData::None, } } @@ -239,8 +209,8 @@ impl BuildTemplate { } /// Returns the profession of this build. - pub fn profession(&self) -> Profession { - self.profession + pub fn profession(&self) -> &Profession { + &self.profession } /// Returns the skills of this build. @@ -273,7 +243,7 @@ impl BuildTemplate { /// 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; + let prof_byte = self.profession().code as u8; bytes.push(prof_byte); for traitline in self.traitlines().iter() { @@ -295,9 +265,8 @@ impl BuildTemplate { bytes.push(0); } Some(s) => { - let palette_id = skill_id_to_palette_id(s.id); - bytes.push((palette_id & 0xFF) as u8); - bytes.push(((palette_id >> 8) & 0xFF) as u8); + let palette_id = self.profession().skill_id_to_palette_id(s.id).unwrap_or(0); + bytes.write_u16::<LE>(palette_id as u16).unwrap(); } } // Aquatic @@ -314,7 +283,10 @@ impl BuildTemplate { } } - // Weird padding, achieved by looking at other chat links. + // 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); } @@ -338,12 +310,14 @@ impl BuildTemplate { } bytes.remove(0); - let profession = Profession::try_from(bytes.remove(0))?; + let mut reader = Cursor::new(bytes); - let mut traitlines = BuildTemplate::empty(profession).traitlines; + 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 = bytes.remove(0); - let trait_choices = bytes.remove(0); + let spec_id = reader.read_u8()?; + let trait_choices = reader.read_u8()?; if spec_id == 0 { continue; } @@ -355,28 +329,27 @@ impl BuildTemplate { *i = Some((spec, [c_0, c_1, c_2])); } - let mut skills = BuildTemplate::empty(profession).skills; + let mut skills = EMPTY_SKILLS; for i in skills.iter_mut() { // 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; + let palette_id = reader.read_u16::<LE>()? as u32; if palette_id != 0 { - let skill_id = palette_id_to_skill_id(palette_id); + 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 - bytes.remove(0); - bytes.remove(0); + reader.read_u16::<LE>()?; } - let extra_data = match profession { - Profession::Revenant => { + 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(bytes.remove(0))?; + *i = Legend::try_from(reader.read_u8()?)?; } ExtraData::Legends(legends) } @@ -392,29 +365,10 @@ impl BuildTemplate { } } -lazy_static! { - static ref PALETTE_MAPPING: Vec<(u32, u32)> = - serde_json::from_str(include_str!("skill_palette.json")).unwrap(); -} - -// Those functions do linear searches, but the list only has about 400 items, which should be okay. -// If performance becomes an issue, we can always create hash tables or do a binary search, -// however, since we need both directions, we would need double the memory to keep the second map. - -fn skill_id_to_palette_id(input: u32) -> u32 { - for (skill, palette) in PALETTE_MAPPING.iter() { - if *skill == input { - return *palette; - } - } - 0 -} - -fn palette_id_to_skill_id(input: u32) -> u32 { - for (skill, palette) in PALETTE_MAPPING.iter() { - if *palette == input { - return *skill; - } - } - 0 +fn code_to_profession(api: &mut Api, code: u32) -> Result<Profession, ChatlinkError> { + let professions = api.get_all_professions()?; + professions + .into_iter() + .find(|p| p.code == code) + .ok_or(ChatlinkError::MalformedInput) } |