aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Cargo.toml1
-rw-r--r--src/bt.rs112
-rw-r--r--src/main.rs30
3 files changed, 131 insertions, 12 deletions
diff --git a/Cargo.toml b/Cargo.toml
index 5799fa6..7407654 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -20,3 +20,4 @@ itertools = "0.8.0"
md5 = "0.7"
base64 = "0.11"
termcolor = "1.0"
+num_enum = "0.4"
diff --git a/src/bt.rs b/src/bt.rs
index 2a6453d..bb2c5a1 100644
--- a/src/bt.rs
+++ b/src/bt.rs
@@ -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();
+ }
}
}
}